Repository: pypa/pip
Pull request #11875 (2023-03-19 13:19:39)

Simplify direct_url.json generation
Now that we have `req.download_info` that is guaranteed to be present we can simply use it.



Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7a37d18c5..2fc455b9d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/'
 
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.3.0
+  rev: v4.4.0
   hooks:
   - id: check-builtin-literals
   - id: check-added-large-files
@@ -17,18 +17,18 @@ repos:
     exclude: .patch
 
 - repo: https://github.com/psf/black
-  rev: 22.6.0
+  rev: 23.1.0
   hooks:
   - id: black
 
 - repo: https://github.com/PyCQA/flake8
-  rev: 4.0.1
+  rev: 6.0.0
   hooks:
   - id: flake8
     additional_dependencies: [
-        'flake8-bugbear==22.10.27',
-        'flake8-logging-format==0.9.0',
-        'flake8-implicit-str-concat==0.3.0',
+        'flake8-bugbear',
+        'flake8-logging-format',
+        'flake8-implicit-str-concat',
     ]
     exclude: tests/data
 
@@ -56,7 +56,7 @@ repos:
     ]
 
 - repo: https://github.com/pre-commit/pygrep-hooks
-  rev: v1.9.0
+  rev: v1.10.0
   hooks:
   - id: python-no-log-warn
   - id: python-no-eval
diff --git a/docs/html/getting-started.md b/docs/html/getting-started.md
index 0967b0eb9..2b3f0bc93 100644
--- a/docs/html/getting-started.md
+++ b/docs/html/getting-started.md
@@ -98,5 +98,5 @@ Successfully uninstalled sampleproject
 ## Next Steps
 
 It is recommended to learn about what virtual environments are and how to use
-them. This is covered in the ["Installing Packages"](pypug:tutorials/installing-packages)
+them. This is covered in the {doc}`Installing Packages <pypug:tutorials/installing-packages>`
 tutorial on packaging.python.org.
diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py
index f398b7d09..2e5597022 100644
--- a/docs/pip_sphinxext.py
+++ b/docs/pip_sphinxext.py
@@ -254,7 +254,6 @@ class PipCLIDirective(rst.Directive):
         lines = []
         # Create a tab for each OS
         for os, variant in os_variants.items():
-
             # Unpack the values
             prompt = variant["prompt"]
             highlighter = variant["highlighter"]
diff --git a/docs/requirements.txt b/docs/requirements.txt
index fa3a7390c..ef72c8fb7 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,4 @@
-sphinx ~= 4.2, != 4.4.0
+sphinx ~= 6.0
 towncrier
 furo
 myst_parser
diff --git a/news/11719.bugfix.rst b/news/11719.bugfix.rst
new file mode 100644
index 000000000..c2ae8bc1d
--- /dev/null
+++ b/news/11719.bugfix.rst
@@ -0,0 +1,1 @@
+Normalize paths before checking if installed scripts are on PATH.
diff --git a/noxfile.py b/noxfile.py
index 5c4683b7d..565a50399 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -133,6 +133,7 @@ def docs(session: nox.Session) -> None:
         # fmt: off
         return [
             "sphinx-build",
+            "--keep-going",
             "-W",
             "-c", "docs/html",  # see note above
             "-d", "docs/build/doctrees/" + kind,
diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py
index c5f03302d..e96d2b492 100644
--- a/src/pip/_internal/commands/cache.py
+++ b/src/pip/_internal/commands/cache.py
@@ -37,7 +37,6 @@ class CacheCommand(Command):
     """
 
     def add_options(self) -> None:
-
         self.cmd_opts.add_option(
             "--format",
             action="store",
diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py
index 3864220b2..584df9f55 100644
--- a/src/pip/_internal/commands/check.py
+++ b/src/pip/_internal/commands/check.py
@@ -20,7 +20,6 @@ class CheckCommand(Command):
       %prog [options]"""
 
     def run(self, options: Values, args: List[str]) -> int:
-
         package_set, parsing_probs = create_package_set_from_installed()
         missing, conflicting = check_package_set(package_set)
 
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 90388d118..36e947c8c 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -76,7 +76,6 @@ class DownloadCommand(RequirementCommand):
 
     @with_cleanup
     def run(self, options: Values, args: List[str]) -> int:
-
         options.ignore_installed = True
         # editable doesn't really make sense for `pip download`, but the bowels
         # of the RequirementSet code require that property.
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index a8483559c..ef3c487ea 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -43,7 +43,6 @@ class WheelCommand(RequirementCommand):
       %prog [options] <archive url/path> ..."""
 
     def add_options(self) -> None:
-
         self.cmd_opts.add_option(
             "-w",
             "--wheel-dir",
diff --git a/src/pip/_internal/index/sources.py b/src/pip/_internal/index/sources.py
index eec3f12f7..cd9cb8d40 100644
--- a/src/pip/_internal/index/sources.py
+++ b/src/pip/_internal/index/sources.py
@@ -171,7 +171,6 @@ def build_source(
     expand_dir: bool,
     cache_link_parsing: bool,
 ) -> Tuple[Optional[str], Optional[LinkSource]]:
-
     path: Optional[str] = None
     url: Optional[str] = None
     if os.path.exists(location):  # Is a local path.
diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py
index a64af7389..fe61e8116 100644
--- a/src/pip/_internal/models/search_scope.py
+++ b/src/pip/_internal/models/search_scope.py
@@ -79,7 +79,6 @@ class SearchScope:
         redacted_index_urls = []
         if self.index_urls and self.index_urls != [PyPI.simple_url]:
             for url in self.index_urls:
-
                 redacted_index_url = redact_auth_from_url(url)
 
                 # Parse the URL
diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py
index e512ac784..6c40ade15 100644
--- a/src/pip/_internal/network/session.py
+++ b/src/pip/_internal/network/session.py
@@ -316,7 +316,6 @@ class InsecureCacheControlAdapter(CacheControlAdapter):
 
 
 class PipSession(requests.Session):
-
     timeout: Optional[int] = None
 
     def __init__(
diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py
index 0b108d0ca..38bd54276 100644
--- a/src/pip/_internal/operations/install/legacy.py
+++ b/src/pip/_internal/operations/install/legacy.py
@@ -69,7 +69,6 @@ def install(
     unpacked_source_directory: str,
     req_description: str,
 ) -> bool:
-
     header_dir = scheme.headers
 
     with TempDirectory(kind="record") as temp_dir:
diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py
index c79941398..a8cd1330f 100644
--- a/src/pip/_internal/operations/install/wheel.py
+++ b/src/pip/_internal/operations/install/wheel.py
@@ -143,16 +143,18 @@ def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
 
     # We don't want to warn for directories that are on PATH.
     not_warn_dirs = [
-        os.path.normcase(i).rstrip(os.sep)
+        os.path.normcase(os.path.normpath(i)).rstrip(os.sep)
         for i in os.environ.get("PATH", "").split(os.pathsep)
     ]
     # If an executable sits with sys.executable, we don't warn for it.
     #     This covers the case of venv invocations without activating the venv.
-    not_warn_dirs.append(os.path.normcase(os.path.dirname(sys.executable)))
+    not_warn_dirs.append(
+        os.path.normcase(os.path.normpath(os.path.dirname(sys.executable)))
+    )
     warn_for: Dict[str, Set[str]] = {
         parent_dir: scripts
         for parent_dir, scripts in grouped_by_dir.items()
-        if os.path.normcase(parent_dir) not in not_warn_dirs
+        if os.path.normcase(os.path.normpath(parent_dir)) not in not_warn_dirs
     }
     if not warn_for:
         return None
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index 37dbd32e7..dc82a7e4f 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -211,7 +211,6 @@ def install_req_from_editable(
     permit_editable_wheels: bool = False,
     config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
 ) -> InstallRequirement:
-
     parts = parse_req_from_editable(editable_req)
 
     return InstallRequirement(
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index f8f07b0cd..aced95e64 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -165,7 +165,6 @@ def handle_requirement_line(
     line: ParsedLine,
     options: Optional[optparse.Values] = None,
 ) -> ParsedRequirement:
-
     # preserve for the nested code path
     line_comes_from = "{} {} (line {})".format(
         "-c" if line.constraint else "-r",
@@ -210,7 +209,6 @@ def handle_option_line(
     options: Optional[optparse.Values] = None,
     session: Optional[PipSession] = None,
 ) -> None:
-
     if options:
         # percolate options upward
         if opts.require_hashes:
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index f0b7c5bcb..940dbe02b 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -44,10 +44,6 @@ from pip._internal.operations.install.wheel import install_wheel
 from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
 from pip._internal.req.req_uninstall import UninstallPathSet
 from pip._internal.utils.deprecation import LegacyInstallReason, deprecated
-from pip._internal.utils.direct_url_helpers import (
-    direct_url_for_editable,
-    direct_url_from_link,
-)
 from pip._internal.utils.hashes import Hashes
 from pip._internal.utils.misc import (
     ConfiguredBuildBackendHookCaller,
@@ -779,16 +775,6 @@ class InstallRequirement:
 
         if self.is_wheel:
             assert self.local_file_path
-            direct_url = None
-            # TODO this can be refactored to direct_url = self.download_info
-            if self.editable:
-                direct_url = direct_url_for_editable(self.unpacked_source_directory)
-            elif self.original_link:
-                direct_url = direct_url_from_link(
-                    self.original_link,
-                    self.source_dir,
-                    self.original_link_is_in_wheel_cache,
-                )
             install_wheel(
                 self.name,
                 self.local_file_path,
@@ -796,7 +782,7 @@ class InstallRequirement:
                 req_description=str(self.req),
                 pycompile=pycompile,
                 warn_script_location=warn_script_location,
-                direct_url=direct_url,
+                direct_url=self.download_info if self.original_link else None,
                 requested=self.user_supplied,
             )
             self.install_succeeded = True
@@ -850,7 +836,6 @@ class InstallRequirement:
 
 
 def check_invalid_constraint_type(req: InstallRequirement) -> str:
-
     # Check for unsupported forms
     problem = ""
     if not req.name:
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index a4c24b52a..0ad4641b1 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -632,7 +632,6 @@ class Factory:
         e: "ResolutionImpossible[Requirement, Candidate]",
         constraints: Dict[str, Constraint],
     ) -> InstallationError:
-
         assert e.causes, "Installation error reported with no cause"
 
         # If one of the things we can't solve is "we need Python X.Y",
diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py
index f561f1f1e..06addc0dd 100644
--- a/src/pip/_internal/resolution/resolvelib/requirements.py
+++ b/src/pip/_internal/resolution/resolvelib/requirements.py
@@ -64,7 +64,6 @@ class SpecifierRequirement(Requirement):
         return format_name(self.project_name, self._extras)
 
     def format_for_error(self) -> str:
-
         # Convert comma-separated specifiers into "A, B, ..., F and G"
         # This makes the specifier a bit more "human readable", without
         # risking a change in meaning. (Hopefully! Not all edge cases have


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 13011f4fd..57dd7e68a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -432,7 +432,6 @@ def virtualenv_template(
     wheel_install: Path,
     coverage_install: Path,
 ) -> Iterator[VirtualEnvironment]:
-
     venv_type: VirtualEnvironmentType
     if request.config.getoption("--use-venv"):
         venv_type = "venv"
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 93a6b930f..22a71cd32 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -106,7 +106,6 @@ def test_build_env_allow_only_one_install(script: PipTestEnvironment) -> None:
 
 
 def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
-
     create_basic_wheel_for_package(script, "foo", "2.0")
     create_basic_wheel_for_package(script, "bar", "1.0")
     create_basic_wheel_for_package(script, "bar", "3.0")
@@ -206,7 +205,6 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
 
 @pytest.mark.usefixtures("enable_user_site")
 def test_build_env_isolation(script: PipTestEnvironment) -> None:
-
     # Create dummy `pkg` wheel.
     pkg_whl = create_basic_wheel_for_package(script, "pkg", "1.0")
 
diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py
index 7d20f5e31..788abdd2b 100644
--- a/tests/functional/test_cache.py
+++ b/tests/functional/test_cache.py
@@ -107,7 +107,7 @@ def list_matches_wheel(wheel_name: str, result: TestPipResult) -> bool:
           `- foo-1.2.3-py3-none-any.whl `."""
     lines = result.stdout.splitlines()
     expected = f" - {wheel_name}-py3-none-any.whl "
-    return any(map(lambda l: l.startswith(expected), lines))
+    return any(map(lambda line: line.startswith(expected), lines))
 
 
 def list_matches_wheel_abspath(wheel_name: str, result: TestPipResult) -> bool:
@@ -121,7 +121,9 @@ def list_matches_wheel_abspath(wheel_name: str, result: TestPipResult) -> bool:
     expected = f"{wheel_name}-py3-none-any.whl"
     return any(
         map(
-            lambda l: os.path.basename(l).startswith(expected) and os.path.exists(l),
+            lambda line: (
+                os.path.basename(line).startswith(expected) and os.path.exists(line)
+            ),
             lines,
         )
     )
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 97ccfa911..16ef572b5 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -2217,7 +2217,6 @@ def test_install_skip_work_dir_pkg(script: PipTestEnvironment, data: TestData) -
 def test_install_verify_package_name_normalization(
     script: PipTestEnvironment, package_name: str
 ) -> None:
-
     """
     Test that install of a package again using a name which
     normalizes to the original package name, is a no-op
diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py
index 71a50fca6..daec5fc6c 100644
--- a/tests/unit/test_base_command.py
+++ b/tests/unit/test_base_command.py
@@ -22,7 +22,6 @@ def fixed_time() -> Iterator[None]:
 
 
 class FakeCommand(Command):
-
     _name = "fake"
 
     def __init__(
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index 47307c00e..26a2ce4b9 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -1014,6 +1014,7 @@ def test_link_collector_create_find_links_expansion(
     """
     Test "~" expansion in --find-links paths.
     """
+
     # This is a mock version of expanduser() that expands "~" to the tmpdir.
     def expand_path(path: str) -> str:
         if path.startswith("~/"):
diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py
index df4957d59..311be5888 100644
--- a/tests/unit/test_link.py
+++ b/tests/unit/test_link.py
@@ -108,7 +108,7 @@ class TestLink:
     )
     def test_invalid_egg_fragments(self, fragment: str) -> None:
         url = f"git+https://example.com/package#egg={fragment}"
-        with pytest.raises(Exception):
+        with pytest.raises(ValueError):
             Link(url)
 
     @pytest.mark.parametrize(
diff --git a/tests/unit/test_network_cache.py b/tests/unit/test_network_cache.py
index 8764b1343..a5519864f 100644
--- a/tests/unit/test_network_cache.py
+++ b/tests/unit/test_network_cache.py
@@ -24,7 +24,6 @@ class TestSafeFileCache:
     """
 
     def test_cache_roundtrip(self, cache_tmpdir: Path) -> None:
-
         cache = SafeFileCache(os.fspath(cache_tmpdir))
         assert cache.get("test key") is None
         cache.set("test key", b"a test string")
diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py
index 39396512a..9e3a0a5d6 100644
--- a/tests/unit/test_options.py
+++ b/tests/unit/test_options.py
@@ -454,7 +454,6 @@ class TestCountOptions(AddFakeCommandMixin):
 
 
 class TestGeneralOptions(AddFakeCommandMixin):
-
     # the reason to specifically test general options is due to the
     # extra processing they receive, and the number of bugs we've had
 
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 30cbcf71c..be369f1a0 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -613,7 +613,6 @@ class TestBreakOptionsArgs:
 
 
 class TestOptionVariants:
-
     # this suite is really just testing optparse, but added it anyway
 
     def test_variant1(
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 1daaecbf4..a67a7c110 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -53,7 +53,6 @@ class Tests_EgglinkPath:
     "util.egg_link_path_from_location() tests"
 
     def setup_method(self) -> None:
-
         project = "foo"
 
         self.mock_dist = Mock(project_name=project)
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
index c5a8f3be4..6d6d1a3dc 100644
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -518,7 +518,6 @@ class TestInstallUnpackedWheel:
 
 
 class TestMessageAboutScriptsNotOnPATH:
-
     tilde_warning_msg = (
         "NOTE: The current PATH contains path(s) starting with `~`, "
         "which may not be expanded by all applications."
@@ -589,6 +588,12 @@ class TestMessageAboutScriptsNotOnPATH:
         )
         assert retval is None
 
+    def test_PATH_check_path_normalization(self) -> None:
+        retval = self._template(
+            paths=["/a/./b/../b//c/", "/d/e/bin"], scripts=["/a/b/c/foo"]
+        )
+        assert retval is None
+
     def test_single_script__single_dir_on_PATH(self) -> None:
         retval = self._template(paths=["/a/b", "/c/d/bin"], scripts=["/a/b/foo"])
         assert retval is None