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