1import itertools
2import logging
3import os
4import posixpath
5import urllib.parse
6from dataclasses import dataclass
7
8from pip._vendor.packaging.utils import canonicalize_name
9
10from pip._internal.models.index import PyPI
11from pip._internal.utils.compat import has_tls
12from pip._internal.utils.misc import normalize_path, redact_auth_from_url
13
14logger = logging.getLogger(__name__)
15
16
17@dataclass(frozen=True)
18class SearchScope:
19 """
20 Encapsulates the locations that pip is configured to search.
21 """
22
23 __slots__ = ["find_links", "index_urls", "no_index"]
24
25 find_links: list[str]
26 index_urls: list[str]
27 no_index: bool
28
29 @classmethod
30 def create(
31 cls,
32 find_links: list[str],
33 index_urls: list[str],
34 no_index: bool,
35 ) -> "SearchScope":
36 """
37 Create a SearchScope object after normalizing the `find_links`.
38 """
39 # Build find_links. If an argument starts with ~, it may be
40 # a local file relative to a home directory. So try normalizing
41 # it and if it exists, use the normalized version.
42 # This is deliberately conservative - it might be fine just to
43 # blindly normalize anything starting with a ~...
44 built_find_links: list[str] = []
45 for link in find_links:
46 if link.startswith("~"):
47 new_link = normalize_path(link)
48 if os.path.exists(new_link):
49 link = new_link
50 built_find_links.append(link)
51
52 # If we don't have TLS enabled, then WARN if anyplace we're looking
53 # relies on TLS.
54 if not has_tls():
55 for link in itertools.chain(index_urls, built_find_links):
56 parsed = urllib.parse.urlparse(link)
57 if parsed.scheme == "https":
58 logger.warning(
59 "pip is configured with locations that require "
60 "TLS/SSL, however the ssl module in Python is not "
61 "available."
62 )
63 break
64
65 return cls(
66 find_links=built_find_links,
67 index_urls=index_urls,
68 no_index=no_index,
69 )
70
71 def get_formatted_locations(self) -> str:
72 lines = []
73 redacted_index_urls = []
74 if self.index_urls and self.index_urls != [PyPI.simple_url]:
75 for url in self.index_urls:
76 redacted_index_url = redact_auth_from_url(url)
77
78 # Parse the URL
79 purl = urllib.parse.urlsplit(redacted_index_url)
80
81 # URL is generally invalid if scheme and netloc is missing
82 # there are issues with Python and URL parsing, so this test
83 # is a bit crude. See bpo-20271, bpo-23505. Python doesn't
84 # always parse invalid URLs correctly - it should raise
85 # exceptions for malformed URLs
86 if not purl.scheme and not purl.netloc:
87 logger.warning(
88 'The index url "%s" seems invalid, please provide a scheme.',
89 redacted_index_url,
90 )
91
92 redacted_index_urls.append(redacted_index_url)
93
94 lines.append(
95 "Looking in indexes: {}".format(", ".join(redacted_index_urls))
96 )
97
98 if self.find_links:
99 lines.append(
100 "Looking in links: {}".format(
101 ", ".join(redact_auth_from_url(url) for url in self.find_links)
102 )
103 )
104 return "\n".join(lines)
105
106 def get_index_urls_locations(self, project_name: str) -> list[str]:
107 """Returns the locations found via self.index_urls
108
109 Checks the url_name on the main (first in the list) index and
110 use this url_name to produce all locations
111 """
112
113 def mkurl_pypi_url(url: str) -> str:
114 loc = posixpath.join(
115 url, urllib.parse.quote(canonicalize_name(project_name))
116 )
117 # For maximum compatibility with easy_install, ensure the path
118 # ends in a trailing slash. Although this isn't in the spec
119 # (and PyPI can handle it without the slash) some other index
120 # implementations might break if they relied on easy_install's
121 # behavior.
122 if not loc.endswith("/"):
123 loc = loc + "/"
124 return loc
125
126 return [mkurl_pypi_url(url) for url in self.index_urls]