def _analyze_specifiers(self) -> None: lower_bound, upper_bound = Version.MIN, Version.MAX excludes: Set[Version] = set() for spec in self: op, version = _normalize_op_specifier(spec.operator, spec.version) if op in ("==", "==="): lower_bound = version upper_bound = version.bump() break if op == "!=": excludes.add(version) elif op[0] == ">": lower_bound = max(lower_bound, version if op == ">=" else version.bump()) elif op[0] == "<": upper_bound = min(upper_bound, version.bump() if op == "<=" else version) elif op == "~=": new_lower = version.complete() new_upper = version.bump(-2) if new_upper < upper_bound: upper_bound = new_upper if new_lower > lower_bound: lower_bound = new_lower else: raise InvalidPyVersion( f"Unsupported version specifier: {op}{version}") self._rearrange(lower_bound, upper_bound, excludes)
def parse_version_tuple(version: str) -> Tuple[Union[int, str], ...]: version = re.sub(r"(?<!\.)\*", ".*", version) try: return tuple(int(v) if v != "*" else v for v in version.split(".")) except ValueError: raise InvalidPyVersion( f"{version}: Prereleases or postreleases are not supported " "for python version specifers.")
def _analyze_specifiers(self) -> None: # XXX: Prerelease or postrelease specifiers will fail here, but I guess we can # just ignore them for now. lower_bound, upper_bound = self.MIN_VERSION, self.MAX_VERSION excludes = set() # type: Set[Tuple[Union[int, str], ...]] for spec in self: op, version = spec.operator, spec.version version = parse_version_tuple(version) if version[-1] == "*": if op == "==": op = "~=" version = version[:-1] + (0,) elif op == "!=": excludes.add(version) continue if op != "~=": version = _complete_version(version) if op in ("==", "==="): lower_bound = version upper_bound = bump_version(version) break if op == "!=": excludes.add(version) elif op[0] == ">": if op == ">=": new_lower = version else: new_lower = bump_version(version) if new_lower > lower_bound: lower_bound = new_lower elif op[0] == "<": if op == "<=": new_upper = bump_version(version) else: new_upper = version if new_upper < upper_bound: upper_bound = new_upper elif op == "~=": new_lower = _complete_version(version) new_upper = bump_version(version, -2) if new_upper < upper_bound: upper_bound = new_upper if new_lower > lower_bound: lower_bound = new_lower else: raise InvalidPyVersion( f"Unsupported version specifier: {spec.op}{spec.version}" ) self._reorganize(lower_bound, upper_bound, excludes)
def __init__(self, version: Union[Tuple[VersionBit, ...], str]) -> None: if isinstance(version, str): version_str = re.sub(r"(?<!\.)\*", ".*", version) try: version = cast( Tuple[VersionBit, ...], tuple(int(v) if v != "*" else v for v in version_str.split("."))[ :3 ], ) except ValueError: raise InvalidPyVersion( f"{version_str}: Prereleases or postreleases are not supported " "for python version specifers." ) self._version: Tuple[VersionBit, ...] = version
def _analyze_specifiers(self) -> None: # XXX: Prerelease or postrelease specifiers will fail here, but I guess we can # just ignore them for now. lower_bound, upper_bound = Version.MIN, Version.MAX excludes: Set[Version] = set() for spec in self: op, version = _normalize_op_specifier(spec.operator, spec.version) if op in ("==", "==="): lower_bound = version upper_bound = version.bump() break if op == "!=": excludes.add(version) elif op[0] == ">": if op == ">=": new_lower = version else: new_lower = version.bump() if new_lower > lower_bound: lower_bound = new_lower elif op[0] == "<": if op == "<=": new_upper = version.bump() else: new_upper = version if new_upper < upper_bound: upper_bound = new_upper elif op == "~=": new_lower = version.complete() new_upper = version.bump(-2) if new_upper < upper_bound: upper_bound = new_upper if new_lower > lower_bound: lower_bound = new_lower else: raise InvalidPyVersion( f"Unsupported version specifier: {op}{version}") self._rearrange(lower_bound, upper_bound, excludes)
def _normalize_op_specifier(op: str, version_str: str) -> Tuple[str, Version]: version = Version(version_str) if version.is_wildcard: if op == "==": op = "~=" version[-1] = 0 elif op == ">": # >X.Y.* => >=X.Y+1.0 op = ">=" version = version.bump(-2) elif op in ("<", ">=", "<="): # <X.Y.* => <X.Y.0 # >=X.Y.* => >=X.Y.0 # <=X.Y.* => <X.Y.0 version[-1] = 0 if op == "<=": op = "<" elif op != "!=": raise InvalidPyVersion(f"Unsupported version specifier: {op}{version}") if op != "~=" and not (op == "!=" and version.is_wildcard): # Don't complete with .0 for ~=3.5 and !=3.4.*: version = version.complete() return op, version
def __init__(self, version: Union[Tuple[VersionBit, ...], str]) -> None: if isinstance(version, str): version_str = re.sub(r"(?<!\.)\*", ".*", version) bits: List[VersionBit] = [] for v in version_str.split(".")[:3]: try: bits.append(int(v)) except ValueError: pre_m = PRE_RELEASE_SEGMENT_RE.match(v) if v == "*": bits.append("*") break # .* is only allowed at the end, per PEP 440 elif pre_m: bits.append(int(pre_m.group("digit"))) pre_type = pre_m.group("type").lower() pre_n = int(pre_m.group("n") or "0") self.pre = (pre_type, pre_n) break # pre release version is only at the end else: raise InvalidPyVersion( f"{version_str}: postreleases are not supported " "for python version specifiers.") version = tuple(bits) self._version: Tuple[VersionBit, ...] = version
def do_use( project: Project, python: str = "", first: bool = False, ignore_remembered: bool = False, ) -> None: """Use the specified python version and save in project config. The python can be a version string or interpreter path. """ if python: python = python.strip() def version_matcher(py_version: PythonInfo) -> bool: return project.python_requires.contains(str(py_version.version), True) if not project.cache_dir.exists(): project.cache_dir.mkdir(parents=True) use_cache: JSONFileCache[str, str] = JSONFileCache( project.cache_dir / "use_cache.json" ) selected_python: PythonInfo | None = None if python and not ignore_remembered: if use_cache.has_key(python): path = use_cache.get(python) cached_python = PythonInfo.from_path(path) if not cached_python.valid: project.core.ui.echo( f"The last selection is corrupted. {path!r}", fg="red", err=True, ) elif version_matcher(cached_python): project.core.ui.echo( "Using the last selection, add '-i' to ignore it.", fg="yellow", err=True, ) selected_python = cached_python if selected_python is None: found_interpreters = list(dict.fromkeys(project.find_interpreters(python))) matching_interperters = list(filter(version_matcher, found_interpreters)) if not found_interpreters: raise NoPythonVersion("Python interpreter is not found on the system.") if not matching_interperters: project.core.ui.echo("Interpreters found but not matching:", err=True) for py in found_interpreters: project.core.ui.echo(f" - {py.executable} ({py.identifier})", err=True) raise NoPythonVersion( "No python is found meeting the requirement " f"{termui.green('python' + str(project.python_requires))}" ) if first or len(matching_interperters) == 1: selected_python = matching_interperters[0] else: project.core.ui.echo("Please enter the Python interpreter to use") for i, py_version in enumerate(matching_interperters): project.core.ui.echo( f"{i}. {termui.green(str(py_version.executable))} " f"({py_version.identifier})" ) selection = click.prompt( "Please select:", type=click.Choice([str(i) for i in range(len(matching_interperters))]), default="0", show_choices=False, ) selected_python = matching_interperters[int(selection)] if python: use_cache.set(python, selected_python.path.as_posix()) if not selected_python.valid: path = str(selected_python.executable) raise InvalidPyVersion(f"Invalid Python interpreter: {path}") old_python = project.python if "python.path" in project.config else None project.core.ui.echo( "Using Python interpreter: {} ({})".format( termui.green(str(selected_python.executable)), selected_python.identifier, ) ) project.python = selected_python if ( old_python and old_python.path != selected_python.path and not project.environment.is_global ): project.core.ui.echo(termui.cyan("Updating executable scripts...")) project.environment.update_shebangs(selected_python.executable.as_posix())
def from_path(cls, path: str | Path) -> "PythonInfo": try: return cls.from_python_version(PythonVersion.from_path(str(path))) except InvalidPythonVersion as e: raise InvalidPyVersion(str(e)) from e