Files
cibuildwheel/cibuildwheel/util.py
T

706 lines
22 KiB
Python
Raw Normal View History

from __future__ import annotations
2021-02-04 14:27:54 -05:00
import contextlib
import fnmatch
import itertools
2019-11-12 23:51:27 +00:00
import os
2020-12-20 19:42:28 +00:00
import re
import shlex
import ssl
2021-06-23 10:47:18 -04:00
import subprocess
import sys
2020-11-23 21:00:22 +00:00
import textwrap
2021-02-04 14:27:54 -05:00
import time
2020-11-23 21:00:22 +00:00
import urllib.request
2022-04-28 09:19:27 -04:00
from dataclasses import dataclass
2020-11-23 21:00:22 +00:00
from enum import Enum
from functools import lru_cache
2022-04-18 14:24:53 +02:00
from pathlib import Path, PurePath
from time import sleep
from typing import (
Any,
ClassVar,
2022-04-26 22:21:27 -04:00
Generator,
Iterable,
Sequence,
TextIO,
2022-04-18 14:24:53 +02:00
TypeVar,
cast,
overload,
)
import bracex
2020-11-23 21:00:22 +00:00
import certifi
2022-04-15 21:00:57 -07:00
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from filelock import FileLock
from packaging.requirements import InvalidRequirement, Requirement
2021-01-31 17:14:15 -05:00
from packaging.specifiers import SpecifierSet
2022-04-18 14:24:53 +02:00
from packaging.utils import parse_wheel_filename
2021-01-31 17:14:15 -05:00
from packaging.version import Version
from platformdirs import user_cache_path
2020-11-23 21:00:22 +00:00
from cibuildwheel.typing import Final, Literal, PathOrStr, PlatformName
__all__ = [
"resources_dir",
"MANYLINUX_ARCHS",
"call",
"shell",
"find_compatible_wheel",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
"read_python_configs",
"selector_matches",
"strtobool",
"cached_property",
2022-04-26 22:21:27 -04:00
"chdir",
2022-09-06 00:56:20 -04:00
"split_config_settings",
]
2022-09-16 11:05:05 -04:00
resources_dir: Final[Path] = Path(__file__).parent / "resources"
2021-01-31 17:14:15 -05:00
2022-09-16 11:05:05 -04:00
install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
2022-02-12 12:29:58 -05:00
BuildFrontend = Literal["pip", "build"]
2021-06-23 10:47:18 -04:00
2022-09-16 11:05:05 -04:00
MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
2021-09-19 00:19:28 -04:00
"x86_64",
"i686",
"pypy_x86_64",
"aarch64",
"ppc64le",
"s390x",
"pypy_aarch64",
"pypy_i686",
)
2022-09-16 11:05:05 -04:00
MUSLLINUX_ARCHS: Final[tuple[str, ...]] = (
2021-09-19 00:19:28 -04:00
"x86_64",
"i686",
"aarch64",
"ppc64le",
"s390x",
)
2022-09-16 11:05:05 -04:00
DEFAULT_CIBW_CACHE_PATH: Final[Path] = user_cache_path(appname="cibuildwheel", appauthor="pypa")
CIBW_CACHE_PATH: Final[Path] = Path(
os.environ.get("CIBW_CACHE_PATH", DEFAULT_CIBW_CACHE_PATH)
).resolve()
2022-09-16 11:05:05 -04:00
IS_WIN: Final[bool] = sys.platform.startswith("win")
@overload
def call(
*args: PathOrStr,
env: dict[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[False] = ...,
) -> None:
...
@overload
def call(
*args: PathOrStr,
env: dict[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: Literal[True],
) -> str:
...
def call(
*args: PathOrStr,
env: dict[str, str] | None = None,
cwd: PathOrStr | None = None,
capture_stdout: bool = False,
) -> str | None:
"""
Run subprocess.run, but print the commands first. Takes the commands as
*args. Uses shell=True on Windows due to a bug. Also converts to
Paths to strings, due to Windows behavior at least on older Pythons.
https://bugs.python.org/issue8557
"""
args_ = [str(arg) for arg in args]
# print the command executing for the logs
print("+ " + " ".join(shlex.quote(a) for a in args_))
kwargs: dict[str, Any] = {}
if capture_stdout:
kwargs["universal_newlines"] = True
kwargs["stdout"] = subprocess.PIPE
result = subprocess.run(args_, check=True, shell=IS_WIN, env=env, cwd=cwd, **kwargs)
if not capture_stdout:
return None
return cast(str, result.stdout)
def shell(*commands: str, env: dict[str, str] | None = None, cwd: PathOrStr | None = None) -> None:
command = " ".join(commands)
print(f"+ {command}")
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
2022-09-09 08:34:47 -04:00
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
2021-10-21 15:18:01 -04:00
"""
Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched.
>>> format_safe('{a} {b}', a='123')
'123 {b}'
>>> format_safe('{a} {b[4]:3f}', a='123')
'123 {b[4]:3f}'
To avoid variable expansion, precede with a single backslash e.g.
>>> format_safe('\\{a} {b}', a='123')
'{a} {b}'
"""
result = template
for key, value in kwargs.items():
find_pattern = re.compile(
rf"""
2021-11-14 18:43:34 +00:00
(?<!\#) # don't match if preceded by a hash
2021-10-21 15:18:01 -04:00
{{ # literal open curly bracket
{re.escape(key)} # the field name
}} # literal close curly bracket
""",
re.VERBOSE,
)
2021-11-14 13:38:41 +00:00
result = re.sub(
pattern=find_pattern,
2022-09-09 08:34:47 -04:00
repl=str(value).replace("\\", r"\\"),
2021-11-14 13:38:41 +00:00
string=result,
)
2021-10-21 15:18:01 -04:00
# transform escaped sequences into their literal equivalents
2021-11-14 18:43:34 +00:00
result = result.replace(f"#{{{key}}}", f"{{{key}}}")
2021-10-21 15:18:01 -04:00
return result
def prepare_command(command: str, **kwargs: PathOrStr) -> str:
2021-05-03 11:45:43 -04:00
"""
2019-11-12 23:34:59 +00:00
Preprocesses a command by expanding variables like {python}.
2019-11-12 23:34:59 +00:00
For example, used in the test_command option to specify the path to the
2021-10-21 15:18:01 -04:00
project's root. Unmatched syntax will mostly be allowed through.
2021-05-03 11:45:43 -04:00
"""
2021-10-21 15:18:01 -04:00
return format_safe(command, python="python", pip="pip", **kwargs)
2017-04-11 22:57:42 +01:00
def get_build_verbosity_extra_flags(level: int) -> list[str]:
if level > 0:
2021-05-03 11:45:43 -04:00
return ["-" + level * "v"]
elif level < 0:
2021-05-03 11:45:43 -04:00
return ["-" + -level * "q"]
else:
return []
2023-03-10 13:46:42 -05:00
def split_config_settings(config_settings: str, frontend: Literal["pip", "build"]) -> list[str]:
2022-09-06 00:56:20 -04:00
config_settings_list = shlex.split(config_settings)
2023-03-10 13:46:42 -05:00
s = "s" if frontend == "pip" else ""
2023-03-10 12:00:51 -05:00
return [f"--config-setting{s}={setting}" for setting in config_settings_list]
2022-09-06 00:56:20 -04:00
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
2021-05-03 11:45:43 -04:00
input_file = resources_dir / "build-platforms.toml"
2021-10-15 11:30:16 -04:00
with input_file.open("rb") as f:
2022-04-15 21:00:57 -07:00
loaded_file = tomllib.load(f)
results: list[dict[str, str]] = list(loaded_file[config]["python_configurations"])
2021-01-09 15:40:40 -05:00
return results
2021-10-12 02:05:47 +01:00
def selector_matches(patterns: str, string: str) -> bool:
"""
Returns True if `string` is matched by any of the wildcard patterns in
`patterns`.
Matching is according to fnmatch, but with shell-like curly brace
expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
'cp37-*'.
"""
patterns_list = patterns.split()
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list)
return any(fnmatch.fnmatch(string, pat) for pat in expanded_patterns)
2021-10-12 02:05:47 +01:00
# Once we require Python 3.10+, we can add kw_only=True
2022-04-28 09:19:27 -04:00
@dataclass(frozen=True)
2022-06-15 10:12:46 +01:00
class BuildSelector:
2021-01-20 21:22:48 -05:00
"""
This class holds a set of build/skip patterns. You call an instance with a
build identifier, and it returns True if that identifier should be
2021-01-31 17:14:15 -05:00
included. Only call this on valid identifiers, ones that have at least 2
2022-06-15 10:12:46 +01:00
numeric digits before the first dash.
2021-01-20 21:22:48 -05:00
"""
2021-01-31 17:14:15 -05:00
2022-06-15 10:12:46 +01:00
build_config: str
skip_config: str
requires_python: SpecifierSet | None = None
2022-06-15 10:12:46 +01:00
# a pattern that skips prerelease versions, when include_prereleases is False.
PRERELEASE_SKIP: ClassVar[str] = ""
prerelease_pythons: bool = False
2017-04-11 22:57:42 +01:00
def __call__(self, build_id: str) -> bool:
2021-01-31 17:14:15 -05:00
# Filter build selectors by python_requires if set
if self.requires_python is not None:
2021-05-03 11:45:43 -04:00
py_ver_str = build_id.split("-")[0]
2021-01-31 17:14:15 -05:00
major = int(py_ver_str[2])
minor = int(py_ver_str[3:])
version = Version(f"{major}.{minor}.99")
if not self.requires_python.contains(version):
return False
2021-10-12 02:05:47 +01:00
# filter out the prerelease pythons if self.prerelease_pythons is False
if not self.prerelease_pythons and selector_matches(self.PRERELEASE_SKIP, build_id):
2021-10-12 02:05:47 +01:00
return False
2021-10-12 02:05:47 +01:00
should_build = selector_matches(self.build_config, build_id)
should_skip = selector_matches(self.skip_config, build_id)
2021-10-12 02:05:47 +01:00
return should_build and not should_skip
2017-04-13 15:02:04 +01:00
2022-11-26 15:54:08 +00:00
def options_summary(self) -> Any:
return {
"build_config": self.build_config,
"skip_config": self.skip_config,
"requires_python": str(self.requires_python),
"prerelease_pythons": self.prerelease_pythons,
}
2021-01-20 21:22:48 -05:00
2022-04-28 09:19:27 -04:00
@dataclass(frozen=True)
2022-06-15 10:12:46 +01:00
class TestSelector:
"""
A build selector that can only skip tests according to a skip pattern.
"""
2021-01-20 21:22:48 -05:00
2022-06-15 10:12:46 +01:00
skip_config: str
2021-01-20 21:22:48 -05:00
2022-06-15 10:12:46 +01:00
def __call__(self, build_id: str) -> bool:
should_skip = selector_matches(self.skip_config, build_id)
return not should_skip
2022-11-26 15:54:08 +00:00
def options_summary(self) -> Any:
return {"skip_config": self.skip_config}
# Taken from https://stackoverflow.com/a/107717
2020-02-03 20:42:55 +01:00
class Unbuffered:
def __init__(self, stream: TextIO) -> None:
self.stream = stream
def write(self, data: str) -> None:
self.stream.write(data)
self.stream.flush()
def writelines(self, data: Iterable[str]) -> None:
2021-07-14 13:45:04 -04:00
self.stream.writelines(data)
self.stream.flush()
def __getattr__(self, attr: str) -> Any:
return getattr(self.stream, attr)
2020-06-15 01:53:31 +02:00
def download(url: str, dest: Path) -> None:
2021-05-03 11:45:43 -04:00
print(f"+ Download {url} to {dest}")
2020-06-15 01:53:31 +02:00
dest_dir = dest.parent
if not dest_dir.exists():
dest_dir.mkdir(parents=True)
# we've had issues when relying on the host OS' CA certificates on Windows,
# so we use certifi (this sounds odd but requests also does this by default)
2021-05-03 11:45:43 -04:00
cafile = os.environ.get("SSL_CERT_FILE", certifi.where())
context = ssl.create_default_context(cafile=cafile)
repeat_num = 3
for i in range(repeat_num):
try:
with urllib.request.urlopen(url, context=context) as response:
dest.write_bytes(response.read())
return
except OSError:
if i == repeat_num - 1:
raise
sleep(3)
class DependencyConstraints:
2020-06-15 01:53:31 +02:00
def __init__(self, base_file_path: Path):
assert base_file_path.exists()
self.base_file_path = base_file_path.resolve()
2020-04-10 20:24:52 +02:00
@staticmethod
def with_defaults() -> DependencyConstraints:
2021-05-03 11:45:43 -04:00
return DependencyConstraints(base_file_path=resources_dir / "constraints.txt")
2020-06-15 01:53:31 +02:00
def get_for_python_version(self, version: str) -> Path:
2021-05-03 11:45:43 -04:00
version_parts = version.split(".")
# try to find a version-specific dependency file e.g. if
2021-02-14 20:44:20 +01:00
# ./constraints.txt is the base, look for ./constraints-python36.txt
2021-05-03 11:45:43 -04:00
specific_stem = self.base_file_path.stem + f"-python{version_parts[0]}{version_parts[1]}"
2020-06-17 00:18:40 +02:00
specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name)
2020-06-15 01:53:31 +02:00
if specific_file_path.exists():
return specific_file_path
else:
return self.base_file_path
def __repr__(self) -> str:
2021-10-12 02:05:47 +01:00
return f"{self.__class__.__name__}({self.base_file_path!r})"
2021-10-12 02:05:47 +01:00
def __eq__(self, o: object) -> bool:
if not isinstance(o, DependencyConstraints):
return False
2021-10-12 02:05:47 +01:00
return self.base_file_path == o.base_file_path
2021-09-19 00:19:28 -04:00
2022-11-26 15:54:08 +00:00
def options_summary(self) -> Any:
if self == DependencyConstraints.with_defaults():
return "pinned"
else:
return self.base_file_path.name
2021-09-19 00:19:28 -04:00
class NonPlatformWheelError(Exception):
def __init__(self) -> None:
2021-04-30 17:56:34 -04:00
message = textwrap.dedent(
2021-05-03 11:45:43 -04:00
"""
cibuildwheel: Build failed because a pure Python wheel was generated.
If you intend to build a pure-Python wheel, you don't need cibuildwheel - use
`pip wheel -w DEST_DIR .` instead.
If you expected a platform wheel, check your project configuration, or run
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
)
super().__init__(message)
2020-11-23 21:00:22 +00:00
class AlreadyBuiltWheelError(Exception):
def __init__(self, wheel_name: str) -> None:
message = textwrap.dedent(
f"""
cibuildwheel: Build failed because a wheel named {wheel_name} was already generated in the current run.
If you expected another wheel to be generated, check your project configuration, or run
cibuildwheel with CIBW_BUILD_VERBOSITY=1 to view build logs.
"""
)
super().__init__(message)
2020-11-23 21:00:22 +00:00
def strtobool(val: str) -> bool:
2021-05-03 11:45:43 -04:00
return val.lower() in {"y", "yes", "t", "true", "on", "1"}
2020-11-23 21:00:22 +00:00
2020-12-31 16:35:26 +00:00
class CIProvider(Enum):
2021-05-03 11:45:43 -04:00
travis_ci = "travis"
appveyor = "appveyor"
circle_ci = "circle_ci"
azure_pipelines = "azure_pipelines"
github_actions = "github_actions"
gitlab = "gitlab"
2022-07-24 01:17:22 +02:00
cirrus_ci = "cirrus_ci"
2021-05-03 11:45:43 -04:00
other = "other"
2020-11-23 21:00:22 +00:00
def detect_ci_provider() -> CIProvider | None:
2021-05-03 11:45:43 -04:00
if "TRAVIS" in os.environ:
2020-11-23 21:00:22 +00:00
return CIProvider.travis_ci
2021-05-03 11:45:43 -04:00
elif "APPVEYOR" in os.environ:
2020-11-23 21:00:22 +00:00
return CIProvider.appveyor
2021-05-03 11:45:43 -04:00
elif "CIRCLECI" in os.environ:
2020-11-23 21:00:22 +00:00
return CIProvider.circle_ci
2021-05-03 11:45:43 -04:00
elif "AZURE_HTTP_USER_AGENT" in os.environ:
2020-11-23 21:00:22 +00:00
return CIProvider.azure_pipelines
2021-05-03 11:45:43 -04:00
elif "GITHUB_ACTIONS" in os.environ:
2020-11-23 21:00:22 +00:00
return CIProvider.github_actions
2021-05-03 11:45:43 -04:00
elif "GITLAB_CI" in os.environ:
2020-11-23 21:00:22 +00:00
return CIProvider.gitlab
2022-07-24 01:17:22 +02:00
elif "CIRRUS_CI" in os.environ:
return CIProvider.cirrus_ci
2021-05-03 11:45:43 -04:00
elif strtobool(os.environ.get("CI", "false")):
2020-11-23 21:00:22 +00:00
return CIProvider.other
else:
return None
2021-01-05 19:02:15 +00:00
def unwrap(text: str) -> str:
2021-05-03 11:45:43 -04:00
"""
2021-01-05 19:02:15 +00:00
Unwraps multi-line text to a single line
2021-05-03 11:45:43 -04:00
"""
# remove initial line indent
text = textwrap.dedent(text)
# remove leading/trailing whitespace
text = text.strip()
# remove consecutive whitespace
2021-05-03 11:45:43 -04:00
return re.sub(r"\s+", " ", text)
2021-02-04 14:27:54 -05:00
2022-04-28 09:19:27 -04:00
@dataclass(frozen=True)
class FileReport:
name: str
size: str
2021-02-04 14:27:54 -05:00
@contextlib.contextmanager
2022-04-26 22:21:27 -04:00
def print_new_wheels(msg: str, output_dir: Path) -> Generator[None, None, None]:
2021-05-03 11:45:43 -04:00
"""
2021-02-04 14:27:54 -05:00
Prints the new items in a directory upon exiting. The message to display
can include {n} for number of wheels, {s} for total number of seconds,
and/or {m} for total number of minutes. Does not print anything if this
exits via exception.
2021-05-03 11:45:43 -04:00
"""
2021-02-04 14:27:54 -05:00
start_time = time.time()
existing_contents = set(output_dir.iterdir())
yield
final_contents = set(output_dir.iterdir())
new_contents = [
FileReport(wheel.name, f"{(wheel.stat().st_size + 1023) // 1024:,d}")
for wheel in final_contents - existing_contents
]
2022-10-07 08:47:31 -04:00
if not new_contents:
return
max_name_len = max(len(f.name) for f in new_contents)
max_size_len = max(len(f.size) for f in new_contents)
2021-02-04 14:27:54 -05:00
n = len(new_contents)
s = time.time() - start_time
m = s / 60
print(
msg.format(n=n, s=s, m=m),
*sorted(
f" {f.name:<{max_name_len}s} {f.size:>{max_size_len}s} kB" for f in new_contents
),
sep="\n",
)
2021-06-23 10:47:18 -04:00
def get_pip_version(env: dict[str, str]) -> str:
versions_output_text = call(
"python", "-m", "pip", "freeze", "--all", capture_stdout=True, env=env
2021-06-23 10:47:18 -04:00
)
(pip_version,) = (
version[5:]
for version in versions_output_text.strip().splitlines()
if version.startswith("pip==")
)
return pip_version
@lru_cache(maxsize=None)
def _ensure_virtualenv() -> Path:
input_file = resources_dir / "virtualenv.toml"
with input_file.open("rb") as f:
2022-04-15 21:00:57 -07:00
loaded_file = tomllib.load(f)
version = str(loaded_file["version"])
url = str(loaded_file["url"])
path = CIBW_CACHE_PATH / f"virtualenv-{version}.pyz"
with FileLock(str(path) + ".lock"):
if not path.exists():
download(url, path)
return path
def _parse_constraints_for_virtualenv(
dependency_constraint_flags: Sequence[PathOrStr],
) -> dict[str, str]:
"""
Parses the constraints file referenced by `dependency_constraint_flags` and returns a dict where
the key is the package name, and the value is the constraint version.
If a package version cannot be found, its value is "embed" meaning that virtualenv will install
its bundled version, already available locally.
The function does not try to be too smart and just handles basic constraints.
If it can't get an exact version, the real constraint will be handled by the
{macos|windows}.setup_python function.
"""
assert len(dependency_constraint_flags) in {0, 2}
packages = ["pip", "setuptools", "wheel"]
constraints_dict = {package: "embed" for package in packages}
if len(dependency_constraint_flags) == 2:
assert dependency_constraint_flags[0] == "-c"
constraint_path = Path(dependency_constraint_flags[1])
assert constraint_path.exists()
with constraint_path.open(encoding="utf-8") as constraint_file:
for line_ in constraint_file:
line = line_.strip()
2022-10-07 08:47:31 -04:00
if not line:
continue
if line.startswith("#"):
continue
try:
requirement = Requirement(line)
package = requirement.name
if (
package not in packages
or requirement.url is not None
or requirement.marker is not None
or len(requirement.extras) != 0
or len(requirement.specifier) != 1
):
continue
specifier = next(iter(requirement.specifier))
if specifier.operator != "==":
continue
constraints_dict[package] = specifier.version
except InvalidRequirement:
continue
return constraints_dict
def virtualenv(
python: Path, venv_path: Path, dependency_constraint_flags: Sequence[PathOrStr]
) -> dict[str, str]:
assert python.exists()
virtualenv_app = _ensure_virtualenv()
constraints = _parse_constraints_for_virtualenv(dependency_constraint_flags)
additional_flags = [f"--{package}={version}" for package, version in constraints.items()]
# Using symlinks to pre-installed seed packages is really the fastest way to get a virtual
# environment. The initial cost is a bit higher but reusing is much faster.
# Windows does not always allow symlinks so just disabling for now.
# Requires pip>=19.3 so disabling for "embed" because this means we don't know what's the
# version of pip that will end-up installed.
# c.f. https://virtualenv.pypa.io/en/latest/cli_interface.html#section-seeder
if (
not IS_WIN
and constraints["pip"] != "embed"
and Version(constraints["pip"]) >= Version("19.3")
):
additional_flags.append("--symlink-app-data")
call(
sys.executable,
"-sS", # just the stdlib, https://github.com/pypa/virtualenv/issues/2133#issuecomment-1003710125
virtualenv_app,
"--activators=",
"--no-periodic-update",
*additional_flags,
"--python",
python,
venv_path,
)
if IS_WIN:
paths = [str(venv_path), str(venv_path / "Scripts")]
else:
paths = [str(venv_path / "bin")]
env = os.environ.copy()
2023-01-30 15:53:44 -05:00
env["PATH"] = os.pathsep.join([*paths, env["PATH"]])
return env
2022-04-18 14:24:53 +02:00
T = TypeVar("T", bound=PurePath)
def find_compatible_wheel(wheels: Sequence[T], identifier: str) -> T | None:
2022-04-18 14:24:53 +02:00
"""
Finds a wheel with an abi3 or a none ABI tag in `wheels` compatible with the Python interpreter
specified by `identifier` that is previously built.
2022-04-18 14:24:53 +02:00
"""
interpreter, platform = identifier.split("-")
for wheel in wheels:
_, _, _, tags = parse_wheel_filename(wheel.name)
for tag in tags:
if tag.abi == "abi3":
# ABI3 wheels must start with cp3 for impl and tag
if not (interpreter.startswith("cp3") and tag.interpreter.startswith("cp3")):
continue
elif tag.abi == "none":
# CPythonless wheels must include py3 tag
if tag.interpreter[:3] != "py3":
continue
else:
# Other types of wheels are not detected, this is looking for previously built wheels.
2022-04-18 14:24:53 +02:00
continue
if tag.interpreter != "py3" and int(tag.interpreter[3:]) > int(interpreter[3:]):
# If a minor version number is given, it has to be lower than the current one.
2022-04-18 14:24:53 +02:00
continue
2022-04-18 14:24:53 +02:00
if platform.startswith(("manylinux", "musllinux", "macosx")):
# Linux, macOS require the beginning and ending match (macos/manylinux version doesn't need to)
2022-04-18 14:24:53 +02:00
os_, arch = platform.split("_", 1)
if not tag.platform.startswith(os_):
continue
if not tag.platform.endswith(f"_{arch}"):
2022-04-18 14:24:53 +02:00
continue
else:
# Windows should exactly match
2023-01-30 15:53:44 -05:00
if tag.platform != platform:
2022-04-18 14:24:53 +02:00
continue
# If all the filters above pass, then the wheel is a previously built compatible wheel.
2022-04-18 14:24:53 +02:00
return wheel
2022-04-18 14:24:53 +02:00
return None
if sys.version_info >= (3, 8):
from functools import cached_property
else:
from .functools_cached_property_38 import cached_property
2022-04-26 22:21:27 -04:00
# Can be replaced by contextlib.chdir in Python 3.11
@contextlib.contextmanager
def chdir(new_path: Path | str) -> Generator[None, None, None]:
2022-04-26 22:21:27 -04:00
"""Non thread-safe context manager to change the current working directory."""
cwd = os.getcwd()
try:
os.chdir(new_path)
yield
finally:
os.chdir(cwd)
2022-12-04 13:51:56 +00:00
def fix_ansi_codes_for_github_actions(text: str) -> str:
"""
Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line.
"""
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
ansi_codes: list[str] = []
output = ""
for line in text.splitlines(keepends=True):
2022-12-04 13:51:56 +00:00
# add the current ANSI codes to the beginning of the line
output += "".join(ansi_codes) + line
2022-12-04 13:51:56 +00:00
# split the line at each ANSI code
parts = ansi_code_regex.split(line)
# if there are any ANSI codes, save them
if len(parts) > 1:
# iterate over the ANSI codes in this line
for code in parts[1::2]:
if code == "\033[0m":
# reset the list of ANSI codes when the clear code is found
ansi_codes = []
else:
ansi_codes.append(code)
return output