Skip to content

Commit 203780b

Browse files
authored
Merge pull request #12865 from pradyunsg/better-exception-handling-around-selfcheck
Better exception handling around self-check
2 parents 8eadcab + e503141 commit 203780b

File tree

4 files changed

+82
-86
lines changed

4 files changed

+82
-86
lines changed

src/pip/_internal/cli/base_command.py

Lines changed: 59 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
"""Base Command class, and related routines"""
22

3-
import functools
43
import logging
54
import logging.config
65
import optparse
76
import os
87
import sys
98
import traceback
109
from optparse import Values
11-
from typing import Any, Callable, List, Optional, Tuple
10+
from typing import List, Optional, Tuple
1211

1312
from pip._vendor.rich import reconfigure
1413
from pip._vendor.rich import traceback as rich_traceback
@@ -91,6 +90,63 @@ def handle_pip_version_check(self, options: Values) -> None:
9190
def run(self, options: Values, args: List[str]) -> int:
9291
raise NotImplementedError
9392

93+
def _run_wrapper(self, level_number: int, options: Values, args: List[str]) -> int:
94+
def _inner_run() -> int:
95+
try:
96+
return self.run(options, args)
97+
finally:
98+
self.handle_pip_version_check(options)
99+
100+
if options.debug_mode:
101+
rich_traceback.install(show_locals=True)
102+
return _inner_run()
103+
104+
try:
105+
status = _inner_run()
106+
assert isinstance(status, int)
107+
return status
108+
except DiagnosticPipError as exc:
109+
logger.error("%s", exc, extra={"rich": True})
110+
logger.debug("Exception information:", exc_info=True)
111+
112+
return ERROR
113+
except PreviousBuildDirError as exc:
114+
logger.critical(str(exc))
115+
logger.debug("Exception information:", exc_info=True)
116+
117+
return PREVIOUS_BUILD_DIR_ERROR
118+
except (
119+
InstallationError,
120+
BadCommand,
121+
NetworkConnectionError,
122+
) as exc:
123+
logger.critical(str(exc))
124+
logger.debug("Exception information:", exc_info=True)
125+
126+
return ERROR
127+
except CommandError as exc:
128+
logger.critical("%s", exc)
129+
logger.debug("Exception information:", exc_info=True)
130+
131+
return ERROR
132+
except BrokenStdoutLoggingError:
133+
# Bypass our logger and write any remaining messages to
134+
# stderr because stdout no longer works.
135+
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
136+
if level_number <= logging.DEBUG:
137+
traceback.print_exc(file=sys.stderr)
138+
139+
return ERROR
140+
except KeyboardInterrupt:
141+
logger.critical("Operation cancelled by user")
142+
logger.debug("Exception information:", exc_info=True)
143+
144+
return ERROR
145+
except BaseException:
146+
logger.critical("Exception:", exc_info=True)
147+
148+
return UNKNOWN_ERROR
149+
94150
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
95151
# factored out for testability
96152
return self.parser.parse_args(args)
@@ -172,65 +228,4 @@ def _main(self, args: List[str]) -> int:
172228
)
173229
options.cache_dir = None
174230

175-
def intercepts_unhandled_exc(
176-
run_func: Callable[..., int]
177-
) -> Callable[..., int]:
178-
@functools.wraps(run_func)
179-
def exc_logging_wrapper(*args: Any) -> int:
180-
try:
181-
status = run_func(*args)
182-
assert isinstance(status, int)
183-
return status
184-
except DiagnosticPipError as exc:
185-
logger.error("%s", exc, extra={"rich": True})
186-
logger.debug("Exception information:", exc_info=True)
187-
188-
return ERROR
189-
except PreviousBuildDirError as exc:
190-
logger.critical(str(exc))
191-
logger.debug("Exception information:", exc_info=True)
192-
193-
return PREVIOUS_BUILD_DIR_ERROR
194-
except (
195-
InstallationError,
196-
BadCommand,
197-
NetworkConnectionError,
198-
) as exc:
199-
logger.critical(str(exc))
200-
logger.debug("Exception information:", exc_info=True)
201-
202-
return ERROR
203-
except CommandError as exc:
204-
logger.critical("%s", exc)
205-
logger.debug("Exception information:", exc_info=True)
206-
207-
return ERROR
208-
except BrokenStdoutLoggingError:
209-
# Bypass our logger and write any remaining messages to
210-
# stderr because stdout no longer works.
211-
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
212-
if level_number <= logging.DEBUG:
213-
traceback.print_exc(file=sys.stderr)
214-
215-
return ERROR
216-
except KeyboardInterrupt:
217-
logger.critical("Operation cancelled by user")
218-
logger.debug("Exception information:", exc_info=True)
219-
220-
return ERROR
221-
except BaseException:
222-
logger.critical("Exception:", exc_info=True)
223-
224-
return UNKNOWN_ERROR
225-
226-
return exc_logging_wrapper
227-
228-
try:
229-
if not options.debug_mode:
230-
run = intercepts_unhandled_exc(self.run)
231-
else:
232-
run = self.run
233-
rich_traceback.install(show_locals=True)
234-
return run(options, args)
235-
finally:
236-
self.handle_pip_version_check(options)
231+
return self._run_wrapper(level_number, options, args)

src/pip/_internal/cli/index_command.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,15 @@ def handle_pip_version_check(self, options: Values) -> None:
156156
if options.disable_pip_version_check or options.no_index:
157157
return
158158

159-
# Otherwise, check if we're using the latest version of pip available.
160-
session = self._build_session(
161-
options,
162-
retries=0,
163-
timeout=min(5, options.timeout),
164-
)
165-
with session:
166-
_pip_self_version_check(session, options)
159+
try:
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+
)
166+
with session:
167+
_pip_self_version_check(session, options)
168+
except Exception:
169+
logger.warning("There was an error checking the latest version of pip.")
170+
logger.debug("See below for error", exc_info=True)

src/pip/_internal/self_outdated_check.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -232,17 +232,13 @@ def pip_self_version_check(session: PipSession, options: optparse.Values) -> Non
232232
if not installed_dist:
233233
return
234234

235-
try:
236-
upgrade_prompt = _self_version_check_logic(
237-
state=SelfCheckState(cache_dir=options.cache_dir),
238-
current_time=datetime.datetime.now(datetime.timezone.utc),
239-
local_version=installed_dist.version,
240-
get_remote_version=functools.partial(
241-
_get_current_remote_pip_version, session, options
242-
),
243-
)
244-
if upgrade_prompt is not None:
245-
logger.warning("%s", upgrade_prompt, extra={"rich": True})
246-
except Exception:
247-
logger.warning("There was an error checking the latest version of pip.")
248-
logger.debug("See below for error", exc_info=True)
235+
upgrade_prompt = _self_version_check_logic(
236+
state=SelfCheckState(cache_dir=options.cache_dir),
237+
current_time=datetime.datetime.now(datetime.timezone.utc),
238+
local_version=installed_dist.version,
239+
get_remote_version=functools.partial(
240+
_get_current_remote_pip_version, session, options
241+
),
242+
)
243+
if upgrade_prompt is not None:
244+
logger.warning("%s", upgrade_prompt, extra={"rich": True})

tests/unit/test_self_check_outdated.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def test_pip_self_version_check_calls_underlying_implementation(
4141
# GIVEN
4242
mock_session = Mock()
4343
fake_options = Values({"cache_dir": str(tmpdir)})
44+
mocked_function.return_value = None
4445

4546
# WHEN
4647
self_outdated_check.pip_self_version_check(mock_session, fake_options)

0 commit comments

Comments
 (0)