Skip to content

Commit 71a08a7

Browse files
authored
Merge pull request #12637 from ichard26/refactor/imports
Avoid network/index related imports for pip uninstall & list (unless necessary)
2 parents 22142d6 + 1071614 commit 71a08a7

File tree

10 files changed

+237
-199
lines changed

10 files changed

+237
-199
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pip uninstall and list currently depend on req_install.py which always imports
2+
the expensive network and index machinery. However, it's only in rare situations
3+
that these commands actually hit the network:
4+
5+
- ``pip list --outdated``
6+
- ``pip list --uptodate``
7+
- ``pip uninstall --requirement <url>``
8+
9+
This patch refactors req_install.py so these commands can avoid the expensive
10+
imports unless truly necessary.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""
2+
Contains command classes which may interact with an index / the network.
3+
4+
Unlike its sister module, req_command, this module still uses lazy imports
5+
so commands which don't always hit the network (e.g. list w/o --outdated or
6+
--uptodate) don't need waste time importing PipSession and friends.
7+
"""
8+
9+
import logging
10+
import os
11+
import sys
12+
from optparse import Values
13+
from typing import TYPE_CHECKING, List, Optional
14+
15+
from pip._internal.cli.base_command import Command
16+
from pip._internal.cli.command_context import CommandContextMixIn
17+
from pip._internal.exceptions import CommandError
18+
19+
if TYPE_CHECKING:
20+
from ssl import SSLContext
21+
22+
from pip._internal.network.session import PipSession
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
28+
if sys.version_info < (3, 10):
29+
raise CommandError("The truststore feature is only available for Python 3.10+")
30+
31+
try:
32+
import ssl
33+
except ImportError:
34+
logger.warning("Disabling truststore since ssl support is missing")
35+
return None
36+
37+
try:
38+
from pip._vendor import truststore
39+
except ImportError as e:
40+
raise CommandError(f"The truststore feature is unavailable: {e}")
41+
42+
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
43+
44+
45+
class SessionCommandMixin(CommandContextMixIn):
46+
"""
47+
A class mixin for command classes needing _build_session().
48+
"""
49+
50+
def __init__(self) -> None:
51+
super().__init__()
52+
self._session: Optional["PipSession"] = None
53+
54+
@classmethod
55+
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
56+
"""Return a list of index urls from user-provided options."""
57+
index_urls = []
58+
if not getattr(options, "no_index", False):
59+
url = getattr(options, "index_url", None)
60+
if url:
61+
index_urls.append(url)
62+
urls = getattr(options, "extra_index_urls", None)
63+
if urls:
64+
index_urls.extend(urls)
65+
# Return None rather than an empty list
66+
return index_urls or None
67+
68+
def get_default_session(self, options: Values) -> "PipSession":
69+
"""Get a default-managed session."""
70+
if self._session is None:
71+
self._session = self.enter_context(self._build_session(options))
72+
# there's no type annotation on requests.Session, so it's
73+
# automatically ContextManager[Any] and self._session becomes Any,
74+
# then https://github.com/python/mypy/issues/7696 kicks in
75+
assert self._session is not None
76+
return self._session
77+
78+
def _build_session(
79+
self,
80+
options: Values,
81+
retries: Optional[int] = None,
82+
timeout: Optional[int] = None,
83+
fallback_to_certifi: bool = False,
84+
) -> "PipSession":
85+
from pip._internal.network.session import PipSession
86+
87+
cache_dir = options.cache_dir
88+
assert not cache_dir or os.path.isabs(cache_dir)
89+
90+
if "truststore" in options.features_enabled:
91+
try:
92+
ssl_context = _create_truststore_ssl_context()
93+
except Exception:
94+
if not fallback_to_certifi:
95+
raise
96+
ssl_context = None
97+
else:
98+
ssl_context = None
99+
100+
session = PipSession(
101+
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
102+
retries=retries if retries is not None else options.retries,
103+
trusted_hosts=options.trusted_hosts,
104+
index_urls=self._get_index_urls(options),
105+
ssl_context=ssl_context,
106+
)
107+
108+
# Handle custom ca-bundles from the user
109+
if options.cert:
110+
session.verify = options.cert
111+
112+
# Handle SSL client certificate
113+
if options.client_cert:
114+
session.cert = options.client_cert
115+
116+
# Handle timeouts
117+
if options.timeout or timeout:
118+
session.timeout = timeout if timeout is not None else options.timeout
119+
120+
# Handle configured proxies
121+
if options.proxy:
122+
session.proxies = {
123+
"http": options.proxy,
124+
"https": options.proxy,
125+
}
126+
session.trust_env = False
127+
128+
# Determine if we can prompt the user for authentication or not
129+
session.auth.prompting = not options.no_input
130+
session.auth.keyring_provider = options.keyring_provider
131+
132+
return session
133+
134+
135+
def _pip_self_version_check(session: "PipSession", options: Values) -> None:
136+
from pip._internal.self_outdated_check import pip_self_version_check as check
137+
138+
check(session, options)
139+
140+
141+
class IndexGroupCommand(Command, SessionCommandMixin):
142+
"""
143+
Abstract base class for commands with the index_group options.
144+
145+
This also corresponds to the commands that permit the pip version check.
146+
"""
147+
148+
def handle_pip_version_check(self, options: Values) -> None:
149+
"""
150+
Do the pip version check if not disabled.
151+
152+
This overrides the default behavior of not doing the check.
153+
"""
154+
# Make sure the index_group options are present.
155+
assert hasattr(options, "no_index")
156+
157+
if options.disable_pip_version_check or options.no_index:
158+
return
159+
160+
# Otherwise, check if we're using the latest version of pip available.
161+
session = self._build_session(
162+
options,
163+
retries=0,
164+
timeout=min(5, options.timeout),
165+
# This is set to ensure the function does not fail when truststore is
166+
# specified in use-feature but cannot be loaded. This usually raises a
167+
# CommandError and shows a nice user-facing error, but this function is not
168+
# called in that try-except block.
169+
fallback_to_certifi=True,
170+
)
171+
with session:
172+
_pip_self_version_check(session, options)

0 commit comments

Comments
 (0)