@@ -1091,8 +1091,12 @@ def _resolve_pyenv_shim(
10911091 return binary
10921092
10931093 @classmethod
1094- def _spawn_from_binary_external (cls , binary ):
1095- # type: (str) -> SpawnedJob[PythonInterpreter]
1094+ def _spawn_from_binary_external (
1095+ cls ,
1096+ binary , # type: str
1097+ ignore_cached = False , # type: bool
1098+ ):
1099+ # type: (...) -> SpawnedJob[PythonInterpreter]
10961100
10971101 def create_interpreter (
10981102 stdout , # type: bytes
@@ -1114,6 +1118,8 @@ def create_interpreter(
11141118 return interpreter
11151119
11161120 cache_dir = InterpreterDir .create (binary )
1121+ if ignore_cached :
1122+ safe_rmtree (cache_dir )
11171123 if os .path .isfile (cache_dir .interp_info_file ):
11181124 try :
11191125 with open (cache_dir .interp_info_file , "rb" ) as fp :
@@ -1221,8 +1227,12 @@ def hashbang_matches(fn):
12211227 return None
12221228
12231229 @classmethod
1224- def _spawn_from_binary (cls , binary ):
1225- # type: (str) -> SpawnedJob[PythonInterpreter]
1230+ def _spawn_from_binary (
1231+ cls ,
1232+ binary , # type: str
1233+ ignore_cached = False , # type: bool
1234+ ):
1235+ # type: (...) -> SpawnedJob[PythonInterpreter]
12261236 canonicalized_binary = cls .canonicalize_path (binary )
12271237 if not os .path .exists (canonicalized_binary ):
12281238 raise cls .InterpreterNotFound (
@@ -1231,16 +1241,17 @@ def _spawn_from_binary(cls, binary):
12311241
12321242 # N.B.: The cache is written as the last step in PythonInterpreter instance initialization.
12331243 cached_interpreter = cls ._PYTHON_INTERPRETER_BY_NORMALIZED_PATH .get (canonicalized_binary )
1234- if cached_interpreter is not None :
1244+ if cached_interpreter is not None and not ignore_cached :
12351245 return SpawnedJob .completed (cached_interpreter )
1236- return cls ._spawn_from_binary_external (canonicalized_binary )
1246+ return cls ._spawn_from_binary_external (canonicalized_binary , ignore_cached = ignore_cached )
12371247
12381248 @classmethod
12391249 def from_binary (
12401250 cls ,
12411251 binary , # type: str
12421252 pyenv = None , # type: Optional[Pyenv]
12431253 cwd = None , # type: Optional[str]
1254+ ignore_cached = False , # type: bool
12441255 ):
12451256 # type: (...) -> PythonInterpreter
12461257 """Create an interpreter from the given `binary`.
@@ -1250,14 +1261,19 @@ def from_binary(
12501261 Auto-detected by default.
12511262 :param cwd: The cwd to use as a base to look for python version files from. The process cwd
12521263 by default.
1264+ :param ignore_cached: If the binary has already been identified, ignore the cached
1265+ identification and re-identify.
12531266 :return: an interpreter created from the given `binary`.
12541267 """
12551268 python = cls ._resolve_pyenv_shim (binary , pyenv = pyenv , cwd = cwd )
12561269 if python is None :
12571270 raise cls .IdentificationError ("The pyenv shim at {} is not active." .format (binary ))
12581271
12591272 try :
1260- return cast (PythonInterpreter , cls ._spawn_from_binary (python ).await_result ())
1273+ return cast (
1274+ PythonInterpreter ,
1275+ cls ._spawn_from_binary (python , ignore_cached = ignore_cached ).await_result (),
1276+ )
12611277 except Job .Error as e :
12621278 raise cls .IdentificationError ("Failed to identify {}: {}" .format (binary , e ))
12631279
@@ -1497,20 +1513,34 @@ def iter_base_candidate_binary_paths(interpreter):
14971513 if is_exe (candidate_binary_path ):
14981514 yield candidate_binary_path
14991515
1500- def is_same_interpreter (interpreter ):
1501- # type: (PythonInterpreter) -> bool
1502- identity = interpreter ._identity
1503- return identity .version == version and identity .abi_tag == abi_tag
1504-
15051516 resolution_path = [] # type: List[str]
15061517 base_interpreter = self
15071518 while base_interpreter .is_venv :
15081519 resolved = None # type: Optional[PythonInterpreter]
1520+ maybe_reinstalled_interpreters = [] # type: List[PythonInterpreter]
15091521 for candidate_path in iter_base_candidate_binary_paths (base_interpreter ):
15101522 resolved_interpreter = self .from_binary (candidate_path )
1511- if is_same_interpreter (resolved_interpreter ):
1512- resolved = resolved_interpreter
1513- break
1523+ if resolved_interpreter .abi_tag == abi_tag :
1524+ if resolved_interpreter .version == version :
1525+ resolved = resolved_interpreter
1526+ break
1527+ else :
1528+ # N.B.: Different patch versions of Python can have the same `python`
1529+ # binary contents and only differ in shared libraries and the stdlib. We
1530+ # guard against that case here (i.e.: a CPython patch version upgrade or
1531+ # downgrade) by busting the cache as a last resort before failing to
1532+ # resolve a base interpreter.
1533+ #
1534+ # See: https://github.com/pex-tool/pex/issues/3113
1535+ maybe_reinstalled_interpreters .append (resolved_interpreter )
1536+ if resolved is None :
1537+ for maybe_reinstalled_interpreter in maybe_reinstalled_interpreters :
1538+ re_resolved_interpreter = self .from_binary (
1539+ maybe_reinstalled_interpreter .binary , ignore_cached = True
1540+ )
1541+ if re_resolved_interpreter .version == version :
1542+ resolved = re_resolved_interpreter
1543+ break
15141544 if resolved is None :
15151545 message = [
15161546 "Failed to resolve the base interpreter for the virtual environment at "
@@ -1528,7 +1558,7 @@ def is_same_interpreter(interpreter):
15281558 )
15291559 )
15301560 raise self .BaseInterpreterResolutionError ("\n " .join (message ))
1531- base_interpreter = resolved_interpreter
1561+ base_interpreter = resolved
15321562 resolution_path .append (base_interpreter .binary )
15331563 return base_interpreter
15341564
@@ -1551,6 +1581,11 @@ def free_threaded(self):
15511581 def python (self ):
15521582 return self ._identity .python
15531583
1584+ @property
1585+ def abi_tag (self ):
1586+ # type: () -> str
1587+ return self ._identity .abi_tag
1588+
15541589 @property
15551590 def version (self ):
15561591 # type: () -> Tuple[int, int, int]
0 commit comments