Skip to content

Commit 55cfa54

Browse files
committed
Move non requirement command classes out of req_command.py
By moving SessionCommandMixin and IndexGroupCommand into their own module, lazy imports can be easily used.
1 parent 50ada46 commit 55cfa54

File tree

5 files changed

+181
-155
lines changed

5 files changed

+181
-155
lines changed
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)

src/pip/_internal/cli/req_command.py

Lines changed: 6 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
"""Contains the Command base classes that depend on PipSession.
1+
"""Contains the RequirementCommand base class.
22
3-
The classes in this module are in a separate module so the commands not
4-
needing download / PackageFinder capability don't unnecessarily import the
3+
This class is in a separate module so the commands that do not always
4+
need PackageFinder capability don't unnecessarily import the
55
PackageFinder machinery and all its vendored dependencies, etc.
66
"""
77

88
import logging
9-
import os
10-
import sys
119
from functools import partial
1210
from optparse import Values
13-
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
11+
from typing import Any, List, Optional, Tuple
1412

1513
from pip._internal.cache import WheelCache
1614
from pip._internal.cli import cmdoptions
17-
from pip._internal.cli.base_command import Command
18-
from pip._internal.cli.command_context import CommandContextMixIn
15+
from pip._internal.cli.index_command import IndexGroupCommand
16+
from pip._internal.cli.index_command import SessionCommandMixin as SessionCommandMixin
1917
from pip._internal.exceptions import CommandError, PreviousBuildDirError
2018
from pip._internal.index.collector import LinkCollector
2119
from pip._internal.index.package_finder import PackageFinder
@@ -33,159 +31,15 @@
3331
from pip._internal.req.req_file import parse_requirements
3432
from pip._internal.req.req_install import InstallRequirement
3533
from pip._internal.resolution.base import BaseResolver
36-
from pip._internal.self_outdated_check import pip_self_version_check
3734
from pip._internal.utils.temp_dir import (
3835
TempDirectory,
3936
TempDirectoryTypeRegistry,
4037
tempdir_kinds,
4138
)
4239

43-
if TYPE_CHECKING:
44-
from ssl import SSLContext
45-
4640
logger = logging.getLogger(__name__)
4741

4842

49-
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
50-
if sys.version_info < (3, 10):
51-
raise CommandError("The truststore feature is only available for Python 3.10+")
52-
53-
try:
54-
import ssl
55-
except ImportError:
56-
logger.warning("Disabling truststore since ssl support is missing")
57-
return None
58-
59-
try:
60-
from pip._vendor import truststore
61-
except ImportError as e:
62-
raise CommandError(f"The truststore feature is unavailable: {e}")
63-
64-
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
65-
66-
67-
class SessionCommandMixin(CommandContextMixIn):
68-
"""
69-
A class mixin for command classes needing _build_session().
70-
"""
71-
72-
def __init__(self) -> None:
73-
super().__init__()
74-
self._session: Optional[PipSession] = None
75-
76-
@classmethod
77-
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
78-
"""Return a list of index urls from user-provided options."""
79-
index_urls = []
80-
if not getattr(options, "no_index", False):
81-
url = getattr(options, "index_url", None)
82-
if url:
83-
index_urls.append(url)
84-
urls = getattr(options, "extra_index_urls", None)
85-
if urls:
86-
index_urls.extend(urls)
87-
# Return None rather than an empty list
88-
return index_urls or None
89-
90-
def get_default_session(self, options: Values) -> PipSession:
91-
"""Get a default-managed session."""
92-
if self._session is None:
93-
self._session = self.enter_context(self._build_session(options))
94-
# there's no type annotation on requests.Session, so it's
95-
# automatically ContextManager[Any] and self._session becomes Any,
96-
# then https://github.com/python/mypy/issues/7696 kicks in
97-
assert self._session is not None
98-
return self._session
99-
100-
def _build_session(
101-
self,
102-
options: Values,
103-
retries: Optional[int] = None,
104-
timeout: Optional[int] = None,
105-
fallback_to_certifi: bool = False,
106-
) -> PipSession:
107-
cache_dir = options.cache_dir
108-
assert not cache_dir or os.path.isabs(cache_dir)
109-
110-
if "truststore" in options.features_enabled:
111-
try:
112-
ssl_context = _create_truststore_ssl_context()
113-
except Exception:
114-
if not fallback_to_certifi:
115-
raise
116-
ssl_context = None
117-
else:
118-
ssl_context = None
119-
120-
session = PipSession(
121-
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
122-
retries=retries if retries is not None else options.retries,
123-
trusted_hosts=options.trusted_hosts,
124-
index_urls=self._get_index_urls(options),
125-
ssl_context=ssl_context,
126-
)
127-
128-
# Handle custom ca-bundles from the user
129-
if options.cert:
130-
session.verify = options.cert
131-
132-
# Handle SSL client certificate
133-
if options.client_cert:
134-
session.cert = options.client_cert
135-
136-
# Handle timeouts
137-
if options.timeout or timeout:
138-
session.timeout = timeout if timeout is not None else options.timeout
139-
140-
# Handle configured proxies
141-
if options.proxy:
142-
session.proxies = {
143-
"http": options.proxy,
144-
"https": options.proxy,
145-
}
146-
session.trust_env = False
147-
148-
# Determine if we can prompt the user for authentication or not
149-
session.auth.prompting = not options.no_input
150-
session.auth.keyring_provider = options.keyring_provider
151-
152-
return session
153-
154-
155-
class IndexGroupCommand(Command, SessionCommandMixin):
156-
"""
157-
Abstract base class for commands with the index_group options.
158-
159-
This also corresponds to the commands that permit the pip version check.
160-
"""
161-
162-
def handle_pip_version_check(self, options: Values) -> None:
163-
"""
164-
Do the pip version check if not disabled.
165-
166-
This overrides the default behavior of not doing the check.
167-
"""
168-
# Make sure the index_group options are present.
169-
assert hasattr(options, "no_index")
170-
171-
if options.disable_pip_version_check or options.no_index:
172-
return
173-
174-
# Otherwise, check if we're using the latest version of pip available.
175-
session = self._build_session(
176-
options,
177-
retries=0,
178-
timeout=min(5, options.timeout),
179-
# This is set to ensure the function does not fail when truststore is
180-
# specified in use-feature but cannot be loaded. This usually raises a
181-
# CommandError and shows a nice user-facing error, but this function is not
182-
# called in that try-except block.
183-
fallback_to_certifi=True,
184-
)
185-
with session:
186-
pip_self_version_check(session, options)
187-
188-
18943
KEEPABLE_TEMPDIR_TYPES = [
19044
tempdir_kinds.BUILD_ENV,
19145
tempdir_kinds.EPHEM_WHEEL_CACHE,

src/pip/_internal/commands/list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pip._vendor.packaging.version import Version
88

99
from pip._internal.cli import cmdoptions
10-
from pip._internal.cli.req_command import IndexGroupCommand
10+
from pip._internal.cli.index_command import IndexGroupCommand
1111
from pip._internal.cli.status_codes import SUCCESS
1212
from pip._internal.exceptions import CommandError
1313
from pip._internal.index.collector import LinkCollector

tests/unit/test_base_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_raise_broken_stdout__debug_logging(
9393
assert "Traceback (most recent call last):" in stderr
9494

9595

96-
@patch("pip._internal.cli.req_command.Command.handle_pip_version_check")
96+
@patch("pip._internal.cli.index_command.Command.handle_pip_version_check")
9797
def test_handle_pip_version_check_called(mock_handle_version_check: Mock) -> None:
9898
"""
9999
Check that Command.handle_pip_version_check() is called.

tests/unit/test_commands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ def has_option_no_index(command: Command) -> bool:
8787
(True, True, False),
8888
],
8989
)
90-
@mock.patch("pip._internal.cli.req_command.pip_self_version_check")
90+
@mock.patch("pip._internal.cli.index_command._pip_self_version_check")
9191
def test_index_group_handle_pip_version_check(
9292
mock_version_check: mock.Mock,
9393
command_name: str,

0 commit comments

Comments
 (0)