Repository: pypa/pip

Issue:
Keyring support broken on Windows when keyring.exe is found on PATH after #11589
### Description

#11589 assumes all OSes have `\n` as the line seperator, that is incorrect for WIndows. Python already has `os.linesep` which should be used instead.

I included the fix in #11029, which currently is part of the [23.0 milestone](https://github.com/pypa/pip/milestone/62).

See https://github.com/pypa/pip/pull/11399#issuecomment-1349983741 for additional context.

### Expected behavior

`os.linesep` should be used so the password used by Pip won't end with `\r` on Windows.

### pip version

unreleased

### Python version

all

### OS

Windows

### How to Reproduce

1. Configure python to use a private repository and be sure to include a username in the url credential section
2. install keyring somewhere and make sure keyring.exe can only be found via PATH lookup and not by importing it as a package
3. make sure pip can find the password for the configured url and username combination only via keyring.exe on the PATH
4. try to pip install something
5. it will fail because Pip does not strip `\r` from the password

### Output

Looking in indexes: <url>
WARNING: 401 Error, Credentials not correct for <url>

### Code of Conduct

- [X] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).

Discussion:

Patch:
diff --git a/docs/html/reference/inspect-report.md b/docs/html/reference/inspect-report.md
index e2c7301fd..50d83365c 100644
--- a/docs/html/reference/inspect-report.md
+++ b/docs/html/reference/inspect-report.md
@@ -10,7 +10,10 @@ environment, including installed distributions.
 
 The report is a JSON object with the following properties:
 
-- `version`: the string `1`. It will change only if
+- `version`: the string `0`, denoting that the inspect command is an experimental
+  feature. This value will change to `1`, when the feature is deemed stable after
+  gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
+  may be introduced in version `1` without notice. After that, it will change only if
   and when backward incompatible changes are introduced, such as removing mandatory
   fields or changing the semantics or data type of existing fields. The introduction of
   backward incompatible changes will follow the usual pip processes such as the
@@ -69,7 +72,7 @@ this (metadata abriged for brevity):
 
 ```json
 {
-  "version": "1",
+  "version": "0",
   "pip_version": "22.2.dev0",
   "installed": [
     {
diff --git a/docs/html/reference/installation-report.md b/docs/html/reference/installation-report.md
index e0f241318..de67528cf 100644
--- a/docs/html/reference/installation-report.md
+++ b/docs/html/reference/installation-report.md
@@ -23,7 +23,10 @@ When considering use cases, please bear in mind that
 
 The report is a JSON object with the following properties:
 
-- `version`: the string `1`. It will change only if
+- `version`: the string `0`, denoting that the installation report is an experimental
+  feature. This value will change to `1`, when the feature is deemed stable after
+  gathering user feedback (likely in pip 22.3 or 23.0). Backward incompatible changes
+  may be introduced in version `1` without notice. After that, it will change only if
   and when backward incompatible changes are introduced, such as removing mandatory
   fields or changing the semantics or data type of existing fields. The introduction of
   backward incompatible changes will follow the usual pip processes such as the
@@ -89,7 +92,7 @@ will produce an output similar to this (metadata abriged for brevity):
 
 ```json
 {
-  "version": "1",
+  "version": "0",
   "pip_version": "22.2",
   "install": [
     {
diff --git a/news/11312.feature.rst b/news/11312.feature.rst
deleted file mode 100644
index 493dde830..000000000
--- a/news/11312.feature.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Change the hashes in the installation report to be a mapping. Emit the
-``archive_info.hashes`` dictionary in ``direct_url.json``.
diff --git a/news/11757.feature.rst b/news/11757.feature.rst
deleted file mode 100644
index 594fb6271..000000000
--- a/news/11757.feature.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-The inspect and installation report formats are now declared stabled, and their version
-has been bumped from ``0`` to ``1``.
diff --git a/src/pip/_internal/commands/inspect.py b/src/pip/_internal/commands/inspect.py
index 27c8fa3d5..a4e359930 100644
--- a/src/pip/_internal/commands/inspect.py
+++ b/src/pip/_internal/commands/inspect.py
@@ -46,6 +46,11 @@ class InspectCommand(Command):
         self.parser.insert_option_group(0, self.cmd_opts)
 
     def run(self, options: Values, args: List[str]) -> int:
+        logger.warning(
+            "pip inspect is currently an experimental command. "
+            "The output format may change in a future release without prior warning."
+        )
+
         cmdoptions.check_list_path_option(options)
         dists = get_environment(options.path).iter_installed_distributions(
             local_only=options.local,
@@ -53,7 +58,7 @@ class InspectCommand(Command):
             skip=set(stdlib_pkgs),
         )
         output = {
-            "version": "1",
+            "version": "0",
             "pip_version": __version__,
             "installed": [self._dist_to_dict(dist) for dist in dists],
             "environment": default_environment(),
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index cecaac2bc..accceeaec 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -417,6 +417,12 @@ class InstallCommand(RequirementCommand):
             )
 
             if options.json_report_file:
+                logger.warning(
+                    "--report is currently an experimental option. "
+                    "The output format may change in a future release "
+                    "without prior warning."
+                )
+
                 report = InstallationReport(requirement_set.requirements_to_install)
                 if options.json_report_file == "-":
                     print_json(data=report.to_dict())
diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py
index 09b540f91..e75feda9c 100644
--- a/src/pip/_internal/models/direct_url.py
+++ b/src/pip/_internal/models/direct_url.py
@@ -103,28 +103,17 @@ class ArchiveInfo:
     def __init__(
         self,
         hash: Optional[str] = None,
-        hashes: Optional[Dict[str, str]] = None,
     ) -> None:
-        if hash is not None:
-            # Auto-populate the hashes key to upgrade to the new format automatically.
-            # We don't back-populate the legacy hash key.
-            hash_name, hash_value = hash.split("=", 1)
-            if hashes is None:
-                hashes = {hash_name: hash_value}
-            elif hash_name not in hash:
-                hashes = hashes.copy()
-                hashes[hash_name] = hash_value
         self.hash = hash
-        self.hashes = hashes
 
     @classmethod
     def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:
         if d is None:
             return None
-        return cls(hash=_get(d, str, "hash"), hashes=_get(d, dict, "hashes"))
+        return cls(hash=_get(d, str, "hash"))
 
     def _to_dict(self) -> Dict[str, Any]:
-        return _filter_none(hash=self.hash, hashes=self.hashes)
+        return _filter_none(hash=self.hash)
 
 
 class DirInfo:
diff --git a/src/pip/_internal/models/installation_report.py b/src/pip/_internal/models/installation_report.py
index b54afb109..965f09523 100644
--- a/src/pip/_internal/models/installation_report.py
+++ b/src/pip/_internal/models/installation_report.py
@@ -38,7 +38,7 @@ class InstallationReport:
 
     def to_dict(self) -> Dict[str, Any]:
         return {
-            "version": "1",
+            "version": "0",
             "pip_version": __version__,
             "install": [
                 self._install_req_to_dict(ireq) for ireq in self._install_requirements
diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py
index 68b5a5f45..c16213268 100644
--- a/src/pip/_internal/network/auth.py
+++ b/src/pip/_internal/network/auth.py
@@ -128,7 +128,7 @@ class KeyRingCliProvider(KeyRingBaseProvider):
         )
         if res.returncode:
             return None
-        return res.stdout.decode("utf-8").strip("\n")
+        return res.stdout.decode("utf-8").strip(os.linesep)
 
     def _set_password(self, service_name: str, username: str, password: str) -> None:
         """Mirror the implementation of keyring.set_password using cli"""
@@ -136,7 +136,7 @@ class KeyRingCliProvider(KeyRingBaseProvider):
             return None
 
         cmd = [self.keyring, "set", service_name, username]
-        input_ = password.encode("utf-8") + b"\n"
+        input_ = (password + os.linesep).encode("utf-8")
         env = os.environ.copy()
         env["PYTHONIOENCODING"] = "utf-8"
         res = subprocess.run(cmd, input=input_, env=env)


Test patch:
diff --git a/tests/functional/test_inspect.py b/tests/functional/test_inspect.py
index 18abf1a46..464bdbaa1 100644
--- a/tests/functional/test_inspect.py
+++ b/tests/functional/test_inspect.py
@@ -28,7 +28,7 @@ def test_inspect_basic(simple_script: PipTestEnvironment) -> None:
     """
     Test default behavior of inspect command.
     """
-    result = simple_script.pip("inspect")
+    result = simple_script.pip("inspect", allow_stderr_warning=True)
     report = json.loads(result.stdout)
     installed = report["installed"]
     assert len(installed) == 4
diff --git a/tests/functional/test_install_report.py b/tests/functional/test_install_report.py
index 70f71e223..b61fd89c6 100644
--- a/tests/functional/test_install_report.py
+++ b/tests/functional/test_install_report.py
@@ -26,6 +26,7 @@ def test_install_report_basic(
         str(shared_data.root / "packages/"),
         "--report",
         str(report_path),
+        allow_stderr_warning=True,
     )
     report = json.loads(report_path.read_text())
     assert "install" in report
@@ -58,6 +59,7 @@ def test_install_report_dep(
         str(shared_data.root / "packages/"),
         "--report",
         str(report_path),
+        allow_stderr_warning=True,
     )
     report = json.loads(report_path.read_text())
     assert len(report["install"]) == 2
@@ -76,6 +78,7 @@ def test_install_report_index(script: PipTestEnvironment, tmp_path: Path) -> Non
         "Paste[openid]==1.7.5.1",
         "--report",
         str(report_path),
+        allow_stderr_warning=True,
     )
     report = json.loads(report_path.read_text())
     assert len(report["install"]) == 2
@@ -111,6 +114,7 @@ def test_install_report_vcs_and_wheel_cache(
         str(cache_dir),
         "--report",
         str(report_path),
+        allow_stderr_warning=True,
     )
     report = json.loads(report_path.read_text())
     assert len(report["install"]) == 1
@@ -138,6 +142,7 @@ def test_install_report_vcs_and_wheel_cache(
         str(cache_dir),
         "--report",
         str(report_path),
+        allow_stderr_warning=True,
     )
     assert "Using cached pip_test_package" in result.stdout
     report = json.loads(report_path.read_text())
@@ -171,6 +176,7 @@ def test_install_report_vcs_editable(
         "#egg=pip-test-package",
         "--report",
         str(report_path),
+        allow_stderr_warning=True,
     )
     report = json.loads(report_path.read_text())
     assert len(report["install"]) == 1
@@ -197,6 +203,11 @@ def test_install_report_to_stdout(
         str(shared_data.root / "packages/"),
         "--report",
         "-",
+        allow_stderr_warning=True,
+    )
+    assert result.stderr == (
+        "WARNING: --report is currently an experimental option. "
+        "The output format may change in a future release without prior warning.\n"
     )
     report = json.loads(result.stdout)
     assert "install" in report
diff --git a/tests/unit/test_direct_url.py b/tests/unit/test_direct_url.py
index e1708ae93..c81e51292 100644
--- a/tests/unit/test_direct_url.py
+++ b/tests/unit/test_direct_url.py
@@ -39,10 +39,6 @@ def test_archive_info() -> None:
     assert (
         direct_url.info.hash == direct_url_dict["archive_info"]["hash"]  # type: ignore
     )
-    # test we add the hashes key automatically
-    direct_url_dict["archive_info"]["hashes"] = {  # type: ignore
-        "sha1": "1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
-    }
     assert direct_url.to_dict() == direct_url_dict
 
 
diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py
index 692ee299c..3ab253462 100644
--- a/tests/unit/test_direct_url_helpers.py
+++ b/tests/unit/test_direct_url_helpers.py
@@ -146,10 +146,6 @@ def test_from_link_archive() -> None:
     )
     assert isinstance(direct_url.info, ArchiveInfo)
     assert direct_url.info.hash == "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
-    # Test the hashes key has been automatically populated.
-    assert direct_url.info.hashes == {
-        "sha1": "1b8c5bc61a86f377fea47b4276c8c8a5842d2220"
-    }
 
 
 def test_from_link_dir(tmpdir: Path) -> None:
diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py
index 625a20a48..5e9e325a1 100644
--- a/tests/unit/test_network_auth.py
+++ b/tests/unit/test_network_auth.py
@@ -1,4 +1,5 @@
 import functools
+import os
 import sys
 from typing import Any, Dict, Iterable, List, Optional, Tuple
 
@@ -360,7 +361,7 @@ class KeyringSubprocessResult(KeyringModuleV1):
                 self.returncode = 1
             else:
                 # Passwords are returned encoded with a newline appended
-                self.stdout = password.encode("utf-8") + b"\n"
+                self.stdout = (password + os.linesep).encode("utf-8")
 
         if cmd[1] == "set":
             assert stdin is None
@@ -369,7 +370,7 @@ class KeyringSubprocessResult(KeyringModuleV1):
             assert input is not None
 
             # Input from stdin is encoded
-            self.set_password(cmd[2], cmd[3], input.decode("utf-8").strip("\n"))
+            self.set_password(cmd[2], cmd[3], input.decode("utf-8").strip(os.linesep))
 
         return self