Repository: python-poetry/poetry

Issue:
JSONDecodeError on Windows when running Python produces warnings
### Symptom
Running any poetry command would produce the following error:

```
Expecting value: line 1 column 1 (char 0)
````

### Diagnosis
**Under certain circumstances, running Python produces warnings which cannot simply be silenced by `-W ignore`. This confuses parsing methods.**

By looking with -vvv, I debugged the issue.  

In my case, the output looked like this, with the warning included:
```
'\'\\\\wsl.localhost\\Ubuntu-20.04\\home\\adys\\src\'\r\nCMD.EXE was started with the above path as the current directory.\r\nUNC paths are not supported.  Defaulting to Windows directory.\r\n{"implementation_name": "cpython", "implementation_version": "3.10.0", "os_name": "nt", "platform_machine": "AMD64", "platform_release": "10", "platform_system": "Windows", "platform_version": "10.0.22621", "python_full_version": "3.10.0", "platform_python_implementation": "CPython", "python_version": "3.10", "sys_platform": "win32", "version_info": [3, 10, 0, "final", 0], "interpreter_name": "cp", "interpreter_version": "3_10"}\r\n'
```

In poetry/utils/env.py, this causes `run_python_script()` to include those warnings in the output (the return value). The output of `run_python_script()` is parsed very strictly by `sys_path`, `get_version_info`, `get_supported_tags`, `get_marker_env`, etc.

In none of the cases above should the warning be included in the returned output value.

### Resolution

`run_python_script` calls `run()`, which is a generic way of getting a command and running it using subprocess. However, further down the stack, the stderr gets merged with the stdout.

To solve the issue, I added a `stderr` argument to `_run()`, defaulting to its old behaviour. But `run_python_script` now specifies it, setting it to `subprocess.DEVNULL`. This silences other warnings which were not caught by `-W ignore` (I suppose a similar issue happened before, with python-specific warnings, causing this to be included).

Will include a PR.


Discussion:

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
@@ -463,13 +463,16 @@ class EnvCommandError(EnvError):
     def __init__(self, e: CalledProcessError, input: str | None = None) -> None:
         self.e = e
 
-        message = (
-            f"Command {e.cmd} errored with the following return code {e.returncode},"
-            f" and output: \n{decode(e.output)}"
-        )
+        message_parts = [
+            f"Command {e.cmd} errored with the following return code {e.returncode}"
+        ]
+        if e.output:
+            message_parts.append(f"Output:\n{decode(e.output)}")
+        if e.stderr:
+            message_parts.append(f"Error output:\n{decode(e.stderr)}")
         if input:
-            message += f"input was : {input}"
-        super().__init__(message)
+            message_parts.append(f"Input:\n{input}")
+        super().__init__("\n\n".join(message_parts))
 
 
 class NoCompatiblePythonVersionFound(EnvError):
@@ -1503,7 +1506,14 @@ def run_pip(self, *args: str, **kwargs: Any) -> int | str:
 
     def run_python_script(self, content: str, **kwargs: Any) -> int | str:
         return self.run(
-            self._executable, "-I", "-W", "ignore", "-", input_=content, **kwargs
+            self._executable,
+            "-I",
+            "-W",
+            "ignore",
+            "-",
+            input_=content,
+            stderr=subprocess.PIPE,
+            **kwargs,
         )
 
     def _run(self, cmd: list[str], **kwargs: Any) -> int | str:
@@ -1513,23 +1523,24 @@ def _run(self, cmd: list[str], **kwargs: Any) -> int | str:
         call = kwargs.pop("call", False)
         input_ = kwargs.pop("input_", None)
         env = kwargs.pop("env", dict(os.environ))
+        stderr = kwargs.pop("stderr", subprocess.STDOUT)
 
         try:
             if input_:
                 output = subprocess.run(
                     cmd,
                     stdout=subprocess.PIPE,
-                    stderr=subprocess.STDOUT,
+                    stderr=stderr,
                     input=encode(input_),
                     check=True,
                     **kwargs,
                 ).stdout
             elif call:
-                return subprocess.call(cmd, stderr=subprocess.STDOUT, env=env, **kwargs)
-            else:
-                output = subprocess.check_output(
-                    cmd, stderr=subprocess.STDOUT, env=env, **kwargs
+                return subprocess.call(
+                    cmd, stdout=subprocess.PIPE, stderr=stderr, env=env, **kwargs
                 )
+            else:
+                output = subprocess.check_output(cmd, stderr=stderr, env=env, **kwargs)
         except CalledProcessError as e:
             raise EnvCommandError(e, input=input_)
 


Test patch:
diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py
--- a/tests/utils/test_env.py
+++ b/tests/utils/test_env.py
@@ -947,35 +947,89 @@ def test_run_with_called_process_error(
     tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture
 ):
     mocker.patch(
-        "subprocess.run", side_effect=subprocess.CalledProcessError(42, "some_command")
+        "subprocess.run",
+        side_effect=subprocess.CalledProcessError(
+            42, "some_command", "some output", "some error"
+        ),
     )
-    with pytest.raises(EnvCommandError):
+    with pytest.raises(EnvCommandError) as error:
         tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT)
     subprocess.run.assert_called_once()
+    assert "some output" in str(error.value)
+    assert "some error" in str(error.value)
 
 
 def test_call_with_input_and_called_process_error(
     tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture
 ):
     mocker.patch(
-        "subprocess.run", side_effect=subprocess.CalledProcessError(42, "some_command")
+        "subprocess.run",
+        side_effect=subprocess.CalledProcessError(
+            42, "some_command", "some output", "some error"
+        ),
     )
     kwargs = {"call": True}
-    with pytest.raises(EnvCommandError):
+    with pytest.raises(EnvCommandError) as error:
         tmp_venv.run("python", "-", input_=MINIMAL_SCRIPT, **kwargs)
     subprocess.run.assert_called_once()
+    assert "some output" in str(error.value)
+    assert "some error" in str(error.value)
 
 
 def test_call_no_input_with_called_process_error(
     tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture
 ):
     mocker.patch(
-        "subprocess.call", side_effect=subprocess.CalledProcessError(42, "some_command")
+        "subprocess.call",
+        side_effect=subprocess.CalledProcessError(
+            42, "some_command", "some output", "some error"
+        ),
     )
     kwargs = {"call": True}
-    with pytest.raises(EnvCommandError):
+    with pytest.raises(EnvCommandError) as error:
         tmp_venv.run("python", "-", **kwargs)
     subprocess.call.assert_called_once()
+    assert "some output" in str(error.value)
+    assert "some error" in str(error.value)
+
+
+def test_check_output_with_called_process_error(
+    tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture
+):
+    mocker.patch(
+        "subprocess.check_output",
+        side_effect=subprocess.CalledProcessError(
+            42, "some_command", "some output", "some error"
+        ),
+    )
+    with pytest.raises(EnvCommandError) as error:
+        tmp_venv.run("python", "-")
+    subprocess.check_output.assert_called_once()
+    assert "some output" in str(error.value)
+    assert "some error" in str(error.value)
+
+
+def test_run_python_script_called_process_error(
+    tmp_dir: str, tmp_venv: VirtualEnv, mocker: MockerFixture
+):
+    mocker.patch(
+        "subprocess.run",
+        side_effect=subprocess.CalledProcessError(
+            42, "some_command", "some output", "some error"
+        ),
+    )
+    with pytest.raises(EnvCommandError) as error:
+        tmp_venv.run_python_script(MINIMAL_SCRIPT)
+    assert "some output" in str(error.value)
+    assert "some error" in str(error.value)
+
+
+def test_run_python_script_only_stdout(tmp_dir: str, tmp_venv: VirtualEnv):
+    output = tmp_venv.run_python_script(
+        "import sys; print('some warning', file=sys.stderr); print('some output')"
+    )
+    assert "some output" in output
+    assert "some warning" not in output
 
 
 def test_create_venv_tries_to_find_a_compatible_python_executable_using_generic_ones_first(  # noqa: E501