This commit is contained in:
ton
2024-10-07 10:13:40 +07:00
parent aa1631742f
commit 3a7d696db6
9729 changed files with 1832837 additions and 161742 deletions

View File

@@ -1,3 +1,3 @@
from __future__ import annotations
__version__ = "0.41.2"
__version__ = "0.44.0"

View File

@@ -0,0 +1,604 @@
"""
Create a wheel (.whl) distribution.
A wheel is a built archive format.
"""
from __future__ import annotations
import os
import re
import shutil
import stat
import struct
import sys
import sysconfig
import warnings
from email.generator import BytesGenerator, Generator
from email.policy import EmailPolicy
from glob import iglob
from shutil import rmtree
from typing import TYPE_CHECKING, Callable, Iterable, Literal, Sequence, cast
from zipfile import ZIP_DEFLATED, ZIP_STORED
import setuptools
from setuptools import Command
from . import __version__ as wheel_version
from .metadata import pkginfo_to_metadata
from .util import log
from .vendored.packaging import tags
from .vendored.packaging import version as _packaging_version
from .wheelfile import WheelFile
if TYPE_CHECKING:
import types
def safe_name(name: str) -> str:
"""Convert an arbitrary string to a standard distribution name
Any runs of non-alphanumeric/. characters are replaced with a single '-'.
"""
return re.sub("[^A-Za-z0-9.]+", "-", name)
def safe_version(version: str) -> str:
"""
Convert an arbitrary string to a standard version string
"""
try:
# normalize the version
return str(_packaging_version.Version(version))
except _packaging_version.InvalidVersion:
version = version.replace(" ", ".")
return re.sub("[^A-Za-z0-9.]+", "-", version)
setuptools_major_version = int(setuptools.__version__.split(".")[0])
PY_LIMITED_API_PATTERN = r"cp3\d"
def _is_32bit_interpreter() -> bool:
return struct.calcsize("P") == 4
def python_tag() -> str:
return f"py{sys.version_info[0]}"
def get_platform(archive_root: str | None) -> str:
"""Return our platform name 'win32', 'linux_x86_64'"""
result = sysconfig.get_platform()
if result.startswith("macosx") and archive_root is not None:
from .macosx_libfile import calculate_macosx_platform_tag
result = calculate_macosx_platform_tag(archive_root, result)
elif _is_32bit_interpreter():
if result == "linux-x86_64":
# pip pull request #3497
result = "linux-i686"
elif result == "linux-aarch64":
# packaging pull request #234
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
result = "linux-armv7l"
return result.replace("-", "_")
def get_flag(
var: str, fallback: bool, expected: bool = True, warn: bool = True
) -> bool:
"""Use a fallback value for determining SOABI flags if the needed config
var is unset or unavailable."""
val = sysconfig.get_config_var(var)
if val is None:
if warn:
warnings.warn(
f"Config variable '{var}' is unset, Python ABI tag may be incorrect",
RuntimeWarning,
stacklevel=2,
)
return fallback
return val == expected
def get_abi_tag() -> str | None:
"""Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""
soabi: str = sysconfig.get_config_var("SOABI")
impl = tags.interpreter_name()
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):
d = "d"
if get_flag(
"WITH_PYMALLOC",
impl == "cp",
warn=(impl == "cp" and sys.version_info < (3, 8)),
) and sys.version_info < (3, 8):
m = "m"
abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"
elif soabi and impl == "cp" and soabi.startswith("cpython"):
# non-Windows
abi = "cp" + soabi.split("-")[1]
elif soabi and impl == "cp" and soabi.startswith("cp"):
# Windows
abi = soabi.split("-")[0]
elif soabi and impl == "pp":
# we want something like pypy36-pp73
abi = "-".join(soabi.split("-")[:2])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi and impl == "graalpy":
abi = "-".join(soabi.split("-")[:3])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi:
abi = soabi.replace(".", "_").replace("-", "_")
else:
abi = None
return abi
def safer_name(name: str) -> str:
return safe_name(name).replace("-", "_")
def safer_version(version: str) -> str:
return safe_version(version).replace("-", "_")
def remove_readonly(
func: Callable[..., object],
path: str,
excinfo: tuple[type[Exception], Exception, types.TracebackType],
) -> None:
remove_readonly_exc(func, path, excinfo[1])
def remove_readonly_exc(func: Callable[..., object], path: str, exc: Exception) -> None:
os.chmod(path, stat.S_IWRITE)
func(path)
class bdist_wheel(Command):
description = "create a wheel distribution"
supported_compressions = {
"stored": ZIP_STORED,
"deflated": ZIP_DEFLATED,
}
user_options = [
("bdist-dir=", "b", "temporary directory for creating the distribution"),
(
"plat-name=",
"p",
"platform name to embed in generated filenames "
f"(default: {get_platform(None)})",
),
(
"keep-temp",
"k",
"keep the pseudo-installation tree around after "
"creating the distribution archive",
),
("dist-dir=", "d", "directory to put final built distributions in"),
("skip-build", None, "skip rebuilding everything (for testing/debugging)"),
(
"relative",
None,
"build the archive using relative paths (default: false)",
),
(
"owner=",
"u",
"Owner name used when creating a tar file [default: current user]",
),
(
"group=",
"g",
"Group name used when creating a tar file [default: current group]",
),
("universal", None, "make a universal wheel (default: false)"),
(
"compression=",
None,
"zipfile compression (one of: {}) (default: 'deflated')".format(
", ".join(supported_compressions)
),
),
(
"python-tag=",
None,
f"Python implementation compatibility tag (default: '{python_tag()}')",
),
(
"build-number=",
None,
"Build number for this particular version. "
"As specified in PEP-0427, this must start with a digit. "
"[default: None]",
),
(
"py-limited-api=",
None,
"Python tag (cp32|cp33|cpNN) for abi3 wheel tag (default: false)",
),
]
boolean_options = ["keep-temp", "skip-build", "relative", "universal"]
def initialize_options(self):
self.bdist_dir: str = None
self.data_dir = None
self.plat_name: str | None = None
self.plat_tag = None
self.format = "zip"
self.keep_temp = False
self.dist_dir: str | None = None
self.egginfo_dir = None
self.root_is_pure: bool | None = None
self.skip_build = None
self.relative = False
self.owner = None
self.group = None
self.universal: bool = False
self.compression: str | int = "deflated"
self.python_tag: str = python_tag()
self.build_number: str | None = None
self.py_limited_api: str | Literal[False] = False
self.plat_name_supplied = False
def finalize_options(self):
if self.bdist_dir is None:
bdist_base = self.get_finalized_command("bdist").bdist_base
self.bdist_dir = os.path.join(bdist_base, "wheel")
egg_info = self.distribution.get_command_obj("egg_info")
egg_info.ensure_finalized() # needed for correct `wheel_dist_name`
self.data_dir = self.wheel_dist_name + ".data"
self.plat_name_supplied = self.plat_name is not None
try:
self.compression = self.supported_compressions[self.compression]
except KeyError:
raise ValueError(f"Unsupported compression: {self.compression}") from None
need_options = ("dist_dir", "plat_name", "skip_build")
self.set_undefined_options("bdist", *zip(need_options, need_options))
self.root_is_pure = not (
self.distribution.has_ext_modules() or self.distribution.has_c_libraries()
)
if self.py_limited_api and not re.match(
PY_LIMITED_API_PATTERN, self.py_limited_api
):
raise ValueError(f"py-limited-api must match '{PY_LIMITED_API_PATTERN}'")
# Support legacy [wheel] section for setting universal
wheel = self.distribution.get_option_dict("wheel")
if "universal" in wheel:
# please don't define this in your global configs
log.warning(
"The [wheel] section is deprecated. Use [bdist_wheel] instead.",
)
val = wheel["universal"][1].strip()
if val.lower() in ("1", "true", "yes"):
self.universal = True
if self.build_number is not None and not self.build_number[:1].isdigit():
raise ValueError("Build tag (build-number) must start with a digit.")
@property
def wheel_dist_name(self):
"""Return distribution full name with - replaced with _"""
components = (
safer_name(self.distribution.get_name()),
safer_version(self.distribution.get_version()),
)
if self.build_number:
components += (self.build_number,)
return "-".join(components)
def get_tag(self) -> tuple[str, str, str]:
# bdist sets self.plat_name if unset, we should only use it for purepy
# wheels if the user supplied it.
if self.plat_name_supplied:
plat_name = cast(str, self.plat_name)
elif self.root_is_pure:
plat_name = "any"
else:
# macosx contains system version in platform name so need special handle
if self.plat_name and not self.plat_name.startswith("macosx"):
plat_name = self.plat_name
else:
# on macosx always limit the platform name to comply with any
# c-extension modules in bdist_dir, since the user can specify
# a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake
# on other platforms, and on macosx if there are no c-extension
# modules, use the default platform name.
plat_name = get_platform(self.bdist_dir)
if _is_32bit_interpreter():
if plat_name in ("linux-x86_64", "linux_x86_64"):
plat_name = "linux_i686"
if plat_name in ("linux-aarch64", "linux_aarch64"):
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
plat_name = "linux_armv7l"
plat_name = (
plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")
)
if self.root_is_pure:
if self.universal:
impl = "py2.py3"
else:
impl = self.python_tag
tag = (impl, "none", plat_name)
else:
impl_name = tags.interpreter_name()
impl_ver = tags.interpreter_version()
impl = impl_name + impl_ver
# We don't work on CPython 3.1, 3.0.
if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):
impl = self.py_limited_api
abi_tag = "abi3"
else:
abi_tag = str(get_abi_tag()).lower()
tag = (impl, abi_tag, plat_name)
# issue gh-374: allow overriding plat_name
supported_tags = [
(t.interpreter, t.abi, plat_name) for t in tags.sys_tags()
]
assert (
tag in supported_tags
), f"would build wheel with unsupported tag {tag}"
return tag
def run(self):
build_scripts = self.reinitialize_command("build_scripts")
build_scripts.executable = "python"
build_scripts.force = True
build_ext = self.reinitialize_command("build_ext")
build_ext.inplace = False
if not self.skip_build:
self.run_command("build")
install = self.reinitialize_command("install", reinit_subcommands=True)
install.root = self.bdist_dir
install.compile = False
install.skip_build = self.skip_build
install.warn_dir = False
# A wheel without setuptools scripts is more cross-platform.
# Use the (undocumented) `no_ep` option to setuptools'
# install_scripts command to avoid creating entry point scripts.
install_scripts = self.reinitialize_command("install_scripts")
install_scripts.no_ep = True
# Use a custom scheme for the archive, because we have to decide
# at installation time which scheme to use.
for key in ("headers", "scripts", "data", "purelib", "platlib"):
setattr(install, "install_" + key, os.path.join(self.data_dir, key))
basedir_observed = ""
if os.name == "nt":
# win32 barfs if any of these are ''; could be '.'?
# (distutils.command.install:change_roots bug)
basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))
self.install_libbase = self.install_lib = basedir_observed
setattr(
install,
"install_purelib" if self.root_is_pure else "install_platlib",
basedir_observed,
)
log.info(f"installing to {self.bdist_dir}")
self.run_command("install")
impl_tag, abi_tag, plat_tag = self.get_tag()
archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"
if not self.relative:
archive_root = self.bdist_dir
else:
archive_root = os.path.join(
self.bdist_dir, self._ensure_relative(install.install_base)
)
self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))
distinfo_dirname = (
f"{safer_name(self.distribution.get_name())}-"
f"{safer_version(self.distribution.get_version())}.dist-info"
)
distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)
self.egg2dist(self.egginfo_dir, distinfo_dir)
self.write_wheelfile(distinfo_dir)
# Make the archive
if not os.path.exists(self.dist_dir):
os.makedirs(self.dist_dir)
wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")
with WheelFile(wheel_path, "w", self.compression) as wf:
wf.write_files(archive_root)
# Add to 'Distribution.dist_files' so that the "upload" command works
getattr(self.distribution, "dist_files", []).append(
(
"bdist_wheel",
"{}.{}".format(*sys.version_info[:2]), # like 3.7
wheel_path,
)
)
if not self.keep_temp:
log.info(f"removing {self.bdist_dir}")
if not self.dry_run:
if sys.version_info < (3, 12):
rmtree(self.bdist_dir, onerror=remove_readonly)
else:
rmtree(self.bdist_dir, onexc=remove_readonly_exc)
def write_wheelfile(
self, wheelfile_base: str, generator: str = f"bdist_wheel ({wheel_version})"
):
from email.message import Message
msg = Message()
msg["Wheel-Version"] = "1.0" # of the spec
msg["Generator"] = generator
msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()
if self.build_number is not None:
msg["Build"] = self.build_number
# Doesn't work for bdist_wininst
impl_tag, abi_tag, plat_tag = self.get_tag()
for impl in impl_tag.split("."):
for abi in abi_tag.split("."):
for plat in plat_tag.split("."):
msg["Tag"] = "-".join((impl, abi, plat))
wheelfile_path = os.path.join(wheelfile_base, "WHEEL")
log.info(f"creating {wheelfile_path}")
with open(wheelfile_path, "wb") as f:
BytesGenerator(f, maxheaderlen=0).flatten(msg)
def _ensure_relative(self, path: str) -> str:
# copied from dir_util, deleted
drive, path = os.path.splitdrive(path)
if path[0:1] == os.sep:
path = drive + path[1:]
return path
@property
def license_paths(self) -> Iterable[str]:
if setuptools_major_version >= 57:
# Setuptools has resolved any patterns to actual file names
return self.distribution.metadata.license_files or ()
files: set[str] = set()
metadata = self.distribution.get_option_dict("metadata")
if setuptools_major_version >= 42:
# Setuptools recognizes the license_files option but does not do globbing
patterns = cast(Sequence[str], self.distribution.metadata.license_files)
else:
# Prior to those, wheel is entirely responsible for handling license files
if "license_files" in metadata:
patterns = metadata["license_files"][1].split()
else:
patterns = ()
if "license_file" in metadata:
warnings.warn(
'The "license_file" option is deprecated. Use "license_files" instead.',
DeprecationWarning,
stacklevel=2,
)
files.add(metadata["license_file"][1])
if not files and not patterns and not isinstance(patterns, list):
patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
for pattern in patterns:
for path in iglob(pattern):
if path.endswith("~"):
log.debug(
f'ignoring license file "{path}" as it looks like a backup'
)
continue
if path not in files and os.path.isfile(path):
log.info(
f'adding license file "{path}" (matched pattern "{pattern}")'
)
files.add(path)
return files
def egg2dist(self, egginfo_path: str, distinfo_path: str):
"""Convert an .egg-info directory into a .dist-info directory"""
def adios(p: str) -> None:
"""Appropriately delete directory, file or link."""
if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
shutil.rmtree(p)
elif os.path.exists(p):
os.unlink(p)
adios(distinfo_path)
if not os.path.exists(egginfo_path):
# There is no egg-info. This is probably because the egg-info
# file/directory is not named matching the distribution name used
# to name the archive file. Check for this case and report
# accordingly.
import glob
pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")
possible = glob.glob(pat)
err = f"Egg metadata expected at {egginfo_path} but not found"
if possible:
alt = os.path.basename(possible[0])
err += f" ({alt} found - possible misnamed archive file?)"
raise ValueError(err)
if os.path.isfile(egginfo_path):
# .egg-info is a single file
pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)
os.mkdir(distinfo_path)
else:
# .egg-info is a directory
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)
# ignore common egg metadata that is useless to wheel
shutil.copytree(
egginfo_path,
distinfo_path,
ignore=lambda x, y: {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
},
)
# delete dependency_links if it is only whitespace
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
dependency_links = dependency_links_file.read().strip()
if not dependency_links:
adios(dependency_links_path)
pkg_info_path = os.path.join(distinfo_path, "METADATA")
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
with open(pkg_info_path, "w", encoding="utf-8") as out:
Generator(out, policy=serialization_policy).flatten(pkg_info)
for license_path in self.license_paths:
filename = os.path.basename(license_path)
shutil.copy(license_path, os.path.join(distinfo_path, filename))
adios(egginfo_path)

View File

@@ -5,11 +5,11 @@ import logging
import sys
def _not_warning(record):
def _not_warning(record: logging.LogRecord) -> bool:
return record.levelno < logging.WARNING
def configure():
def configure() -> None:
"""
Configure logging to emit warning and above to stderr
and everything else to stdout. This behavior is provided

View File

@@ -1,593 +1,11 @@
"""
Create a wheel (.whl) distribution.
from warnings import warn
A wheel is a built archive format.
"""
from ._bdist_wheel import bdist_wheel as bdist_wheel
from __future__ import annotations
import os
import re
import shutil
import stat
import struct
import sys
import sysconfig
import warnings
from email.generator import BytesGenerator, Generator
from email.policy import EmailPolicy
from glob import iglob
from io import BytesIO
from shutil import rmtree
from zipfile import ZIP_DEFLATED, ZIP_STORED
import setuptools
from setuptools import Command
from . import __version__ as wheel_version
from .macosx_libfile import calculate_macosx_platform_tag
from .metadata import pkginfo_to_metadata
from .util import log
from .vendored.packaging import tags
from .vendored.packaging import version as _packaging_version
from .wheelfile import WheelFile
def safe_name(name):
"""Convert an arbitrary string to a standard distribution name
Any runs of non-alphanumeric/. characters are replaced with a single '-'.
"""
return re.sub("[^A-Za-z0-9.]+", "-", name)
def safe_version(version):
"""
Convert an arbitrary string to a standard version string
"""
try:
# normalize the version
return str(_packaging_version.Version(version))
except _packaging_version.InvalidVersion:
version = version.replace(" ", ".")
return re.sub("[^A-Za-z0-9.]+", "-", version)
setuptools_major_version = int(setuptools.__version__.split(".")[0])
PY_LIMITED_API_PATTERN = r"cp3\d"
def _is_32bit_interpreter():
return struct.calcsize("P") == 4
def python_tag():
return f"py{sys.version_info[0]}"
def get_platform(archive_root):
"""Return our platform name 'win32', 'linux_x86_64'"""
result = sysconfig.get_platform()
if result.startswith("macosx") and archive_root is not None:
result = calculate_macosx_platform_tag(archive_root, result)
elif _is_32bit_interpreter():
if result == "linux-x86_64":
# pip pull request #3497
result = "linux-i686"
elif result == "linux-aarch64":
# packaging pull request #234
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
result = "linux-armv7l"
return result.replace("-", "_")
def get_flag(var, fallback, expected=True, warn=True):
"""Use a fallback value for determining SOABI flags if the needed config
var is unset or unavailable."""
val = sysconfig.get_config_var(var)
if val is None:
if warn:
warnings.warn(
f"Config variable '{var}' is unset, Python ABI tag may " "be incorrect",
RuntimeWarning,
stacklevel=2,
)
return fallback
return val == expected
def get_abi_tag():
"""Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""
soabi = sysconfig.get_config_var("SOABI")
impl = tags.interpreter_name()
if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):
d = ""
m = ""
u = ""
if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):
d = "d"
if get_flag(
"WITH_PYMALLOC",
impl == "cp",
warn=(impl == "cp" and sys.version_info < (3, 8)),
) and sys.version_info < (3, 8):
m = "m"
abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"
elif soabi and impl == "cp":
abi = "cp" + soabi.split("-")[1]
elif soabi and impl == "pp":
# we want something like pypy36-pp73
abi = "-".join(soabi.split("-")[:2])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi and impl == "graalpy":
abi = "-".join(soabi.split("-")[:3])
abi = abi.replace(".", "_").replace("-", "_")
elif soabi:
abi = soabi.replace(".", "_").replace("-", "_")
else:
abi = None
return abi
def safer_name(name):
return safe_name(name).replace("-", "_")
def safer_version(version):
return safe_version(version).replace("-", "_")
def remove_readonly(func, path, excinfo):
remove_readonly_exc(func, path, excinfo[1])
def remove_readonly_exc(func, path, exc):
os.chmod(path, stat.S_IWRITE)
func(path)
class bdist_wheel(Command):
description = "create a wheel distribution"
supported_compressions = {
"stored": ZIP_STORED,
"deflated": ZIP_DEFLATED,
}
user_options = [
("bdist-dir=", "b", "temporary directory for creating the distribution"),
(
"plat-name=",
"p",
"platform name to embed in generated filenames "
"(default: %s)" % get_platform(None),
),
(
"keep-temp",
"k",
"keep the pseudo-installation tree around after "
"creating the distribution archive",
),
("dist-dir=", "d", "directory to put final built distributions in"),
("skip-build", None, "skip rebuilding everything (for testing/debugging)"),
(
"relative",
None,
"build the archive using relative paths " "(default: false)",
),
(
"owner=",
"u",
"Owner name used when creating a tar file" " [default: current user]",
),
(
"group=",
"g",
"Group name used when creating a tar file" " [default: current group]",
),
("universal", None, "make a universal wheel" " (default: false)"),
(
"compression=",
None,
"zipfile compression (one of: {})"
" (default: 'deflated')".format(", ".join(supported_compressions)),
),
(
"python-tag=",
None,
"Python implementation compatibility tag"
" (default: '%s')" % (python_tag()),
),
(
"build-number=",
None,
"Build number for this particular version. "
"As specified in PEP-0427, this must start with a digit. "
"[default: None]",
),
(
"py-limited-api=",
None,
"Python tag (cp32|cp33|cpNN) for abi3 wheel tag" " (default: false)",
),
]
boolean_options = ["keep-temp", "skip-build", "relative", "universal"]
def initialize_options(self):
self.bdist_dir = None
self.data_dir = None
self.plat_name = None
self.plat_tag = None
self.format = "zip"
self.keep_temp = False
self.dist_dir = None
self.egginfo_dir = None
self.root_is_pure = None
self.skip_build = None
self.relative = False
self.owner = None
self.group = None
self.universal = False
self.compression = "deflated"
self.python_tag = python_tag()
self.build_number = None
self.py_limited_api = False
self.plat_name_supplied = False
def finalize_options(self):
if self.bdist_dir is None:
bdist_base = self.get_finalized_command("bdist").bdist_base
self.bdist_dir = os.path.join(bdist_base, "wheel")
egg_info = self.distribution.get_command_obj("egg_info")
egg_info.ensure_finalized() # needed for correct `wheel_dist_name`
self.data_dir = self.wheel_dist_name + ".data"
self.plat_name_supplied = self.plat_name is not None
try:
self.compression = self.supported_compressions[self.compression]
except KeyError:
raise ValueError(f"Unsupported compression: {self.compression}") from None
need_options = ("dist_dir", "plat_name", "skip_build")
self.set_undefined_options("bdist", *zip(need_options, need_options))
self.root_is_pure = not (
self.distribution.has_ext_modules() or self.distribution.has_c_libraries()
)
if self.py_limited_api and not re.match(
PY_LIMITED_API_PATTERN, self.py_limited_api
):
raise ValueError("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN)
# Support legacy [wheel] section for setting universal
wheel = self.distribution.get_option_dict("wheel")
if "universal" in wheel:
# please don't define this in your global configs
log.warning(
"The [wheel] section is deprecated. Use [bdist_wheel] instead.",
)
val = wheel["universal"][1].strip()
if val.lower() in ("1", "true", "yes"):
self.universal = True
if self.build_number is not None and not self.build_number[:1].isdigit():
raise ValueError("Build tag (build-number) must start with a digit.")
@property
def wheel_dist_name(self):
"""Return distribution full name with - replaced with _"""
components = (
safer_name(self.distribution.get_name()),
safer_version(self.distribution.get_version()),
)
if self.build_number:
components += (self.build_number,)
return "-".join(components)
def get_tag(self):
# bdist sets self.plat_name if unset, we should only use it for purepy
# wheels if the user supplied it.
if self.plat_name_supplied:
plat_name = self.plat_name
elif self.root_is_pure:
plat_name = "any"
else:
# macosx contains system version in platform name so need special handle
if self.plat_name and not self.plat_name.startswith("macosx"):
plat_name = self.plat_name
else:
# on macosx always limit the platform name to comply with any
# c-extension modules in bdist_dir, since the user can specify
# a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake
# on other platforms, and on macosx if there are no c-extension
# modules, use the default platform name.
plat_name = get_platform(self.bdist_dir)
if _is_32bit_interpreter():
if plat_name in ("linux-x86_64", "linux_x86_64"):
plat_name = "linux_i686"
if plat_name in ("linux-aarch64", "linux_aarch64"):
# TODO armv8l, packaging pull request #690 => this did not land
# in pip/packaging yet
plat_name = "linux_armv7l"
plat_name = (
plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")
)
if self.root_is_pure:
if self.universal:
impl = "py2.py3"
else:
impl = self.python_tag
tag = (impl, "none", plat_name)
else:
impl_name = tags.interpreter_name()
impl_ver = tags.interpreter_version()
impl = impl_name + impl_ver
# We don't work on CPython 3.1, 3.0.
if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):
impl = self.py_limited_api
abi_tag = "abi3"
else:
abi_tag = str(get_abi_tag()).lower()
tag = (impl, abi_tag, plat_name)
# issue gh-374: allow overriding plat_name
supported_tags = [
(t.interpreter, t.abi, plat_name) for t in tags.sys_tags()
]
assert (
tag in supported_tags
), f"would build wheel with unsupported tag {tag}"
return tag
def run(self):
build_scripts = self.reinitialize_command("build_scripts")
build_scripts.executable = "python"
build_scripts.force = True
build_ext = self.reinitialize_command("build_ext")
build_ext.inplace = False
if not self.skip_build:
self.run_command("build")
install = self.reinitialize_command("install", reinit_subcommands=True)
install.root = self.bdist_dir
install.compile = False
install.skip_build = self.skip_build
install.warn_dir = False
# A wheel without setuptools scripts is more cross-platform.
# Use the (undocumented) `no_ep` option to setuptools'
# install_scripts command to avoid creating entry point scripts.
install_scripts = self.reinitialize_command("install_scripts")
install_scripts.no_ep = True
# Use a custom scheme for the archive, because we have to decide
# at installation time which scheme to use.
for key in ("headers", "scripts", "data", "purelib", "platlib"):
setattr(install, "install_" + key, os.path.join(self.data_dir, key))
basedir_observed = ""
if os.name == "nt":
# win32 barfs if any of these are ''; could be '.'?
# (distutils.command.install:change_roots bug)
basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))
self.install_libbase = self.install_lib = basedir_observed
setattr(
install,
"install_purelib" if self.root_is_pure else "install_platlib",
basedir_observed,
)
log.info(f"installing to {self.bdist_dir}")
self.run_command("install")
impl_tag, abi_tag, plat_tag = self.get_tag()
archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"
if not self.relative:
archive_root = self.bdist_dir
else:
archive_root = os.path.join(
self.bdist_dir, self._ensure_relative(install.install_base)
)
self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))
distinfo_dirname = "{}-{}.dist-info".format(
safer_name(self.distribution.get_name()),
safer_version(self.distribution.get_version()),
)
distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)
self.egg2dist(self.egginfo_dir, distinfo_dir)
self.write_wheelfile(distinfo_dir)
# Make the archive
if not os.path.exists(self.dist_dir):
os.makedirs(self.dist_dir)
wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")
with WheelFile(wheel_path, "w", self.compression) as wf:
wf.write_files(archive_root)
# Add to 'Distribution.dist_files' so that the "upload" command works
getattr(self.distribution, "dist_files", []).append(
(
"bdist_wheel",
"{}.{}".format(*sys.version_info[:2]), # like 3.7
wheel_path,
)
)
if not self.keep_temp:
log.info(f"removing {self.bdist_dir}")
if not self.dry_run:
if sys.version_info < (3, 12):
rmtree(self.bdist_dir, onerror=remove_readonly)
else:
rmtree(self.bdist_dir, onexc=remove_readonly_exc)
def write_wheelfile(
self, wheelfile_base, generator="bdist_wheel (" + wheel_version + ")"
):
from email.message import Message
msg = Message()
msg["Wheel-Version"] = "1.0" # of the spec
msg["Generator"] = generator
msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()
if self.build_number is not None:
msg["Build"] = self.build_number
# Doesn't work for bdist_wininst
impl_tag, abi_tag, plat_tag = self.get_tag()
for impl in impl_tag.split("."):
for abi in abi_tag.split("."):
for plat in plat_tag.split("."):
msg["Tag"] = "-".join((impl, abi, plat))
wheelfile_path = os.path.join(wheelfile_base, "WHEEL")
log.info(f"creating {wheelfile_path}")
buffer = BytesIO()
BytesGenerator(buffer, maxheaderlen=0).flatten(msg)
with open(wheelfile_path, "wb") as f:
f.write(buffer.getvalue().replace(b"\r\n", b"\r"))
def _ensure_relative(self, path):
# copied from dir_util, deleted
drive, path = os.path.splitdrive(path)
if path[0:1] == os.sep:
path = drive + path[1:]
return path
@property
def license_paths(self):
if setuptools_major_version >= 57:
# Setuptools has resolved any patterns to actual file names
return self.distribution.metadata.license_files or ()
files = set()
metadata = self.distribution.get_option_dict("metadata")
if setuptools_major_version >= 42:
# Setuptools recognizes the license_files option but does not do globbing
patterns = self.distribution.metadata.license_files
else:
# Prior to those, wheel is entirely responsible for handling license files
if "license_files" in metadata:
patterns = metadata["license_files"][1].split()
else:
patterns = ()
if "license_file" in metadata:
warnings.warn(
'The "license_file" option is deprecated. Use "license_files" instead.',
DeprecationWarning,
stacklevel=2,
)
files.add(metadata["license_file"][1])
if not files and not patterns and not isinstance(patterns, list):
patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")
for pattern in patterns:
for path in iglob(pattern):
if path.endswith("~"):
log.debug(
f'ignoring license file "{path}" as it looks like a backup'
)
continue
if path not in files and os.path.isfile(path):
log.info(
f'adding license file "{path}" (matched pattern "{pattern}")'
)
files.add(path)
return files
def egg2dist(self, egginfo_path, distinfo_path):
"""Convert an .egg-info directory into a .dist-info directory"""
def adios(p):
"""Appropriately delete directory, file or link."""
if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):
shutil.rmtree(p)
elif os.path.exists(p):
os.unlink(p)
adios(distinfo_path)
if not os.path.exists(egginfo_path):
# There is no egg-info. This is probably because the egg-info
# file/directory is not named matching the distribution name used
# to name the archive file. Check for this case and report
# accordingly.
import glob
pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")
possible = glob.glob(pat)
err = f"Egg metadata expected at {egginfo_path} but not found"
if possible:
alt = os.path.basename(possible[0])
err += f" ({alt} found - possible misnamed archive file?)"
raise ValueError(err)
if os.path.isfile(egginfo_path):
# .egg-info is a single file
pkginfo_path = egginfo_path
pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)
os.mkdir(distinfo_path)
else:
# .egg-info is a directory
pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")
pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)
# ignore common egg metadata that is useless to wheel
shutil.copytree(
egginfo_path,
distinfo_path,
ignore=lambda x, y: {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
},
)
# delete dependency_links if it is only whitespace
dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")
with open(dependency_links_path, encoding="utf-8") as dependency_links_file:
dependency_links = dependency_links_file.read().strip()
if not dependency_links:
adios(dependency_links_path)
pkg_info_path = os.path.join(distinfo_path, "METADATA")
serialization_policy = EmailPolicy(
utf8=True,
mangle_from_=False,
max_line_length=0,
)
with open(pkg_info_path, "w", encoding="utf-8") as out:
Generator(out, policy=serialization_policy).flatten(pkg_info)
for license_path in self.license_paths:
filename = os.path.basename(license_path)
shutil.copy(license_path, os.path.join(distinfo_path, filename))
adios(egginfo_path)
warn(
"The 'wheel' package is no longer the canonical location of the 'bdist_wheel' "
"command, and will be removed in a future release. Please update to setuptools "
"v70.1 or later which contains an integrated version of this command.",
DeprecationWarning,
stacklevel=1,
)

View File

@@ -14,25 +14,25 @@ class WheelError(Exception):
pass
def unpack_f(args):
def unpack_f(args: argparse.Namespace) -> None:
from .unpack import unpack
unpack(args.wheelfile, args.dest)
def pack_f(args):
def pack_f(args: argparse.Namespace) -> None:
from .pack import pack
pack(args.directory, args.dest_dir, args.build_number)
def convert_f(args):
def convert_f(args: argparse.Namespace) -> None:
from .convert import convert
convert(args.files, args.dest_dir, args.verbose)
def tags_f(args):
def tags_f(args: argparse.Namespace) -> None:
from .tags import tags
names = (
@@ -51,14 +51,14 @@ def tags_f(args):
print(name)
def version_f(args):
def version_f(args: argparse.Namespace) -> None:
from .. import __version__
print("wheel %s" % __version__)
print(f"wheel {__version__}")
def parse_build_tag(build_tag: str) -> str:
if not build_tag[0].isdigit():
if build_tag and not build_tag[0].isdigit():
raise ArgumentTypeError("build tag must begin with a digit")
elif "-" in build_tag:
raise ArgumentTypeError("invalid character ('-') in build tag")

View File

@@ -7,7 +7,7 @@ import tempfile
import zipfile
from glob import iglob
from ..bdist_wheel import bdist_wheel
from .._bdist_wheel import bdist_wheel
from ..wheelfile import WheelFile
from . import WheelError
@@ -42,7 +42,7 @@ class _bdist_wheel_tag(bdist_wheel):
return bdist_wheel.get_tag(self)
def egg2wheel(egg_path: str, dest_dir: str):
def egg2wheel(egg_path: str, dest_dir: str) -> None:
filename = os.path.basename(egg_path)
match = egg_info_re.match(filename)
if not match:
@@ -96,7 +96,7 @@ def egg2wheel(egg_path: str, dest_dir: str):
shutil.rmtree(dir)
def parse_wininst_info(wininfo_name, egginfo_name):
def parse_wininst_info(wininfo_name: str, egginfo_name: str | None):
"""Extract metadata from filenames.
Extracts the 4 metadataitems needed (name, version, pyversion, arch) from
@@ -167,7 +167,7 @@ def parse_wininst_info(wininfo_name, egginfo_name):
return {"name": w_name, "ver": w_ver, "arch": w_arch, "pyver": w_pyver}
def wininst2wheel(path, dest_dir):
def wininst2wheel(path: str, dest_dir: str) -> None:
with zipfile.ZipFile(path) as bdw:
# Search for egg-info in the archive
egginfo_name = None
@@ -189,11 +189,11 @@ def wininst2wheel(path, dest_dir):
paths = {"platlib": ""}
dist_info = "{name}-{ver}".format(**info)
datadir = "%s.data/" % dist_info
datadir = f"{dist_info}.data/"
# rewrite paths to trick ZipFile into extracting an egg
# XXX grab wininst .ini - between .exe, padding, and first zip file.
members = []
members: list[str] = []
egginfo_name = ""
for zipinfo in bdw.infolist():
key, basename = zipinfo.filename.split("/", 1)
@@ -246,7 +246,7 @@ def wininst2wheel(path, dest_dir):
bw.full_tag_supplied = True
bw.full_tag = (pyver, abi, arch)
dist_info_dir = os.path.join(dir, "%s.dist-info" % dist_info)
dist_info_dir = os.path.join(dir, f"{dist_info}.dist-info")
bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir)
bw.write_wheelfile(dist_info_dir, generator="wininst2wheel")
@@ -257,7 +257,7 @@ def wininst2wheel(path, dest_dir):
shutil.rmtree(dir)
def convert(files, dest_dir, verbose):
def convert(files: list[str], dest_dir: str, verbose: bool) -> None:
for pat in files:
for installer in iglob(pat):
if os.path.splitext(installer)[1] == ".egg":

View File

@@ -1,16 +1,18 @@
from __future__ import annotations
import email.policy
import os.path
import re
from email.generator import BytesGenerator
from email.parser import BytesParser
from wheel.cli import WheelError
from wheel.wheelfile import WheelFile
DIST_INFO_RE = re.compile(r"^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))\.dist-info$")
BUILD_NUM_RE = re.compile(rb"Build: (\d\w*)$")
def pack(directory: str, dest_dir: str, build_number: str | None):
def pack(directory: str, dest_dir: str, build_number: str | None) -> None:
"""Repack a previously unpacked wheel directory into a new wheel file.
The .dist-info/WHEEL file must contain one or more tags so that the target
@@ -35,31 +37,29 @@ def pack(directory: str, dest_dir: str, build_number: str | None):
name_version = DIST_INFO_RE.match(dist_info_dir).group("namever")
# Read the tags and the existing build number from .dist-info/WHEEL
existing_build_number = None
wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL")
with open(wheel_file_path, "rb") as f:
tags, existing_build_number = read_tags(f.read())
info = BytesParser(policy=email.policy.compat32).parse(f)
tags: list[str] = info.get_all("Tag", [])
existing_build_number = info.get("Build")
if not tags:
raise WheelError(
"No tags present in {}/WHEEL; cannot determine target wheel "
"filename".format(dist_info_dir)
f"No tags present in {dist_info_dir}/WHEEL; cannot determine target "
f"wheel filename"
)
# Set the wheel file name and add/replace/remove the Build tag in .dist-info/WHEEL
build_number = build_number if build_number is not None else existing_build_number
if build_number is not None:
del info["Build"]
if build_number:
info["Build"] = build_number
name_version += "-" + build_number
if build_number != existing_build_number:
with open(wheel_file_path, "rb+") as f:
wheel_file_content = f.read()
wheel_file_content = set_build_number(wheel_file_content, build_number)
f.seek(0)
f.truncate()
f.write(wheel_file_content)
with open(wheel_file_path, "wb") as f:
BytesGenerator(f, maxheaderlen=0).flatten(info)
# Reassemble the tags for the wheel file
tagline = compute_tagline(tags)
@@ -73,45 +73,6 @@ def pack(directory: str, dest_dir: str, build_number: str | None):
print("OK")
def read_tags(input_str: bytes) -> tuple[list[str], str | None]:
"""Read tags from a string.
:param input_str: A string containing one or more tags, separated by spaces
:return: A list of tags and a list of build tags
"""
tags = []
existing_build_number = None
for line in input_str.splitlines():
if line.startswith(b"Tag: "):
tags.append(line.split(b" ")[1].rstrip().decode("ascii"))
elif line.startswith(b"Build: "):
existing_build_number = line.split(b" ")[1].rstrip().decode("ascii")
return tags, existing_build_number
def set_build_number(wheel_file_content: bytes, build_number: str | None) -> bytes:
"""Compute a build tag and add/replace/remove as necessary.
:param wheel_file_content: The contents of .dist-info/WHEEL
:param build_number: The build tags present in .dist-info/WHEEL
:return: The (modified) contents of .dist-info/WHEEL
"""
replacement = (
("Build: %s\r\n" % build_number).encode("ascii") if build_number else b""
)
wheel_file_content, num_replaced = BUILD_NUM_RE.subn(
replacement, wheel_file_content
)
if not num_replaced:
wheel_file_content += replacement
return wheel_file_content
def compute_tagline(tags: list[str]) -> str:
"""Compute a tagline from a list of tags.

View File

@@ -1,11 +1,12 @@
from __future__ import annotations
import email.policy
import itertools
import os
from collections.abc import Iterable
from email.parser import BytesParser
from ..wheelfile import WheelFile
from .pack import read_tags, set_build_number
def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str]:
@@ -48,6 +49,7 @@ def tags(
assert f.filename, f"{f.filename} must be available"
wheel_info = f.read(f.dist_info_path + "/WHEEL")
info = BytesParser(policy=email.policy.compat32).parsebytes(wheel_info)
original_wheel_name = os.path.basename(f.filename)
namever = f.parsed_filename.group("namever")
@@ -56,7 +58,8 @@ def tags(
original_abi_tags = f.parsed_filename.group("abi").split(".")
original_plat_tags = f.parsed_filename.group("plat").split(".")
tags, existing_build_tag = read_tags(wheel_info)
tags: list[str] = info.get_all("Tag", [])
existing_build_tag = info.get("Build")
impls = {tag.split("-")[0] for tag in tags}
abivers = {tag.split("-")[1] for tag in tags}
@@ -103,12 +106,13 @@ def tags(
final_wheel_name = "-".join(final_tags) + ".whl"
if original_wheel_name != final_wheel_name:
tags = [
f"{a}-{b}-{c}"
for a, b, c in itertools.product(
final_python_tags, final_abi_tags, final_plat_tags
)
]
del info["Tag"], info["Build"]
for a, b, c in itertools.product(
final_python_tags, final_abi_tags, final_plat_tags
):
info["Tag"] = f"{a}-{b}-{c}"
if build:
info["Build"] = build
original_wheel_path = os.path.join(
os.path.dirname(f.filename), original_wheel_name
@@ -125,10 +129,7 @@ def tags(
if item.filename == f.dist_info_path + "/RECORD":
continue
if item.filename == f.dist_info_path + "/WHEEL":
content = fin.read(item)
content = set_tags(content, tags)
content = set_build_number(content, build)
fout.writestr(item, content)
fout.writestr(item, info.as_bytes())
else:
fout.writestr(item, fin.read(item))
@@ -136,18 +137,3 @@ def tags(
os.remove(original_wheel_path)
return final_wheel_name
def set_tags(in_string: bytes, tags: Iterable[str]) -> bytes:
"""Set the tags in the .dist-info/WHEEL file contents.
:param in_string: The string to modify.
:param tags: The tags to set.
"""
lines = [line for line in in_string.splitlines() if not line.startswith(b"Tag:")]
for tag in tags:
lines.append(b"Tag: " + tag.encode("ascii"))
in_string = b"\r\n".join(lines) + b"\r\n"
return in_string

View File

@@ -43,6 +43,13 @@ from __future__ import annotations
import ctypes
import os
import sys
from io import BufferedIOBase
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Union
StrPath = Union[str, os.PathLike[str]]
"""here the needed const and struct from mach-o header files"""
@@ -238,7 +245,7 @@ struct build_version_command {
"""
def swap32(x):
def swap32(x: int) -> int:
return (
((x << 24) & 0xFF000000)
| ((x << 8) & 0x00FF0000)
@@ -247,7 +254,10 @@ def swap32(x):
)
def get_base_class_and_magic_number(lib_file, seek=None):
def get_base_class_and_magic_number(
lib_file: BufferedIOBase,
seek: int | None = None,
) -> tuple[type[ctypes.Structure], int]:
if seek is None:
seek = lib_file.tell()
else:
@@ -271,11 +281,11 @@ def get_base_class_and_magic_number(lib_file, seek=None):
return BaseClass, magic_number
def read_data(struct_class, lib_file):
def read_data(struct_class: type[ctypes.Structure], lib_file: BufferedIOBase):
return struct_class.from_buffer_copy(lib_file.read(ctypes.sizeof(struct_class)))
def extract_macosx_min_system_version(path_to_lib):
def extract_macosx_min_system_version(path_to_lib: str):
with open(path_to_lib, "rb") as lib_file:
BaseClass, magic_number = get_base_class_and_magic_number(lib_file, 0)
if magic_number not in [FAT_MAGIC, FAT_MAGIC_64, MH_MAGIC, MH_MAGIC_64]:
@@ -301,7 +311,7 @@ def extract_macosx_min_system_version(path_to_lib):
read_data(FatArch, lib_file) for _ in range(fat_header.nfat_arch)
]
versions_list = []
versions_list: list[tuple[int, int, int]] = []
for el in fat_arch_list:
try:
version = read_mach_header(lib_file, el.offset)
@@ -333,16 +343,17 @@ def extract_macosx_min_system_version(path_to_lib):
return None
def read_mach_header(lib_file, seek=None):
def read_mach_header(
lib_file: BufferedIOBase,
seek: int | None = None,
) -> tuple[int, int, int] | None:
"""
This funcition parse mach-O header and extract
information about minimal system version
This function parses a Mach-O header and extracts
information about the minimal macOS version.
:param lib_file: reference to opened library file with pointer
"""
if seek is not None:
lib_file.seek(seek)
base_class, magic_number = get_base_class_and_magic_number(lib_file)
base_class, magic_number = get_base_class_and_magic_number(lib_file, seek)
arch = "32" if magic_number == MH_MAGIC else "64"
class SegmentBase(base_class):
@@ -382,14 +393,14 @@ def read_mach_header(lib_file, seek=None):
continue
def parse_version(version):
def parse_version(version: int) -> tuple[int, int, int]:
x = (version & 0xFFFF0000) >> 16
y = (version & 0x0000FF00) >> 8
z = version & 0x000000FF
return x, y, z
def calculate_macosx_platform_tag(archive_root, platform_tag):
def calculate_macosx_platform_tag(archive_root: StrPath, platform_tag: str) -> str:
"""
Calculate proper macosx platform tag basing on files which are included to wheel
@@ -422,7 +433,7 @@ def calculate_macosx_platform_tag(archive_root, platform_tag):
assert len(base_version) == 2
start_version = base_version
versions_dict = {}
versions_dict: dict[str, tuple[int, int]] = {}
for dirpath, _dirnames, filenames in os.walk(archive_root):
for filename in filenames:
if filename.endswith(".dylib") or filename.endswith(".so"):

View File

@@ -1,6 +1,7 @@
"""
Tools for converting old- to new-style metadata.
"""
from __future__ import annotations
import functools
@@ -10,17 +11,17 @@ import re
import textwrap
from email.message import Message
from email.parser import Parser
from typing import Iterator
from typing import Generator, Iterable, Iterator, Literal
from .vendored.packaging.requirements import Requirement
def _nonblank(str):
def _nonblank(str: str) -> bool | Literal[""]:
return str and not str.startswith("#")
@functools.singledispatch
def yield_lines(iterable):
def yield_lines(iterable: Iterable[str]) -> Iterator[str]:
r"""
Yield valid lines of a string or iterable.
>>> list(yield_lines(''))
@@ -38,11 +39,13 @@ def yield_lines(iterable):
@yield_lines.register(str)
def _(text):
def _(text: str) -> Iterator[str]:
return filter(_nonblank, map(str.strip, text.splitlines()))
def split_sections(s):
def split_sections(
s: str | Iterator[str],
) -> Generator[tuple[str | None, list[str]], None, None]:
"""Split a string or iterable thereof into (section, content) pairs
Each ``section`` is a stripped version of the section header ("[section]")
and each ``content`` is a list of stripped lines excluding blank lines and
@@ -50,7 +53,7 @@ def split_sections(s):
header, they're returned in a first ``section`` of ``None``.
"""
section = None
content = []
content: list[str] = []
for line in yield_lines(s):
if line.startswith("["):
if line.endswith("]"):
@@ -67,7 +70,7 @@ def split_sections(s):
yield section, content
def safe_extra(extra):
def safe_extra(extra: str) -> str:
"""Convert an arbitrary string to a standard 'extra' name
Any runs of non-alphanumeric characters are replaced with a single '_',
and the result is always lowercased.
@@ -75,7 +78,7 @@ def safe_extra(extra):
return re.sub("[^A-Za-z0-9.-]+", "_", extra).lower()
def safe_name(name):
def safe_name(name: str) -> str:
"""Convert an arbitrary string to a standard distribution name
Any runs of non-alphanumeric/. characters are replaced with a single '-'.
"""
@@ -84,10 +87,10 @@ def safe_name(name):
def requires_to_requires_dist(requirement: Requirement) -> str:
"""Return the version specifier for a requirement in PEP 345/566 fashion."""
if getattr(requirement, "url", None):
if requirement.url:
return " @ " + requirement.url
requires_dist = []
requires_dist: list[str] = []
for spec in requirement.specifier:
requires_dist.append(spec.operator + spec.version)
@@ -110,7 +113,7 @@ def convert_requirements(requirements: list[str]) -> Iterator[str]:
def generate_requirements(
extras_require: dict[str, list[str]]
extras_require: dict[str | None, list[str]],
) -> Iterator[tuple[str, str]]:
"""
Convert requirements from a setup()-style dictionary to
@@ -130,13 +133,14 @@ def generate_requirements(
yield "Provides-Extra", extra
if condition:
condition = "(" + condition + ") and "
condition += "extra == '%s'" % extra
condition += f"extra == '{extra}'"
if condition:
condition = " ; " + condition
for new_req in convert_requirements(depends):
yield "Requires-Dist", new_req + condition
canonical_req = str(Requirement(new_req + condition))
yield "Requires-Dist", canonical_req
def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message:

View File

@@ -5,7 +5,7 @@ import os
import re
import sys
import warnings
from typing import Dict, Generator, Iterator, NamedTuple, Optional, Tuple
from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple
from ._elffile import EIClass, EIData, ELFFile, EMachine
@@ -14,6 +14,8 @@ EF_ARM_ABI_VER5 = 0x05000000
EF_ARM_ABI_FLOAT_HARD = 0x00000400
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
# as the type for `path` until then.
@contextlib.contextmanager
def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]:
try:
@@ -48,12 +50,21 @@ def _is_linux_i686(executable: str) -> bool:
)
def _have_compatible_abi(executable: str, arch: str) -> bool:
if arch == "armv7l":
def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
if "armv7l" in archs:
return _is_linux_armhf(executable)
if arch == "i686":
if "i686" in archs:
return _is_linux_i686(executable)
return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"}
allowed_archs = {
"x86_64",
"aarch64",
"ppc64",
"ppc64le",
"s390x",
"loongarch64",
"riscv64",
}
return any(arch in allowed_archs for arch in archs)
# If glibc ever changes its major version, we need to know what the last
@@ -79,7 +90,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
try:
# Should be a string like "glibc 2.17".
version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION")
version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION")
assert version_string is not None
_, version = version_string.rsplit()
except (AssertionError, AttributeError, OSError, ValueError):
@@ -156,7 +167,7 @@ def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
return int(m.group("major")), int(m.group("minor"))
@functools.lru_cache()
@functools.lru_cache
def _get_glibc_version() -> Tuple[int, int]:
version_str = _glibc_version_string()
if version_str is None:
@@ -165,13 +176,13 @@ def _get_glibc_version() -> Tuple[int, int]:
# From PEP 513, PEP 600
def _is_compatible(name: str, arch: str, version: _GLibCVersion) -> bool:
def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
sys_glibc = _get_glibc_version()
if sys_glibc < version:
return False
# Check for presence of _manylinux module.
try:
import _manylinux # noqa
import _manylinux
except ImportError:
return True
if hasattr(_manylinux, "manylinux_compatible"):
@@ -201,12 +212,22 @@ _LEGACY_MANYLINUX_MAP = {
}
def platform_tags(linux: str, arch: str) -> Iterator[str]:
if not _have_compatible_abi(sys.executable, arch):
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate manylinux tags compatible to the current platform.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be manylinux-compatible.
:returns: An iterator of compatible manylinux tags.
"""
if not _have_compatible_abi(sys.executable, archs):
return
# Oldest glibc to be supported regardless of architecture is (2, 17).
too_old_glibc2 = _GLibCVersion(2, 16)
if arch in {"x86_64", "i686"}:
if set(archs) & {"x86_64", "i686"}:
# On x86/i686 also oldest glibc to be supported is (2, 5).
too_old_glibc2 = _GLibCVersion(2, 4)
current_glibc = _GLibCVersion(*_get_glibc_version())
@@ -220,19 +241,20 @@ def platform_tags(linux: str, arch: str) -> Iterator[str]:
for glibc_major in range(current_glibc.major - 1, 1, -1):
glibc_minor = _LAST_GLIBC_MINOR[glibc_major]
glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(tag, arch, glibc_version):
yield linux.replace("linux", tag)
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(legacy_tag, arch, glibc_version):
yield linux.replace("linux", legacy_tag)
for arch in archs:
for glibc_max in glibc_max_list:
if glibc_max.major == too_old_glibc2.major:
min_minor = too_old_glibc2.minor
else:
# For other glibc major versions oldest supported is (x, 0).
min_minor = -1
for glibc_minor in range(glibc_max.minor, min_minor, -1):
glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)
tag = "manylinux_{}_{}".format(*glibc_version)
if _is_compatible(arch, glibc_version):
yield f"{tag}_{arch}"
# Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.
if glibc_version in _LEGACY_MANYLINUX_MAP:
legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version]
if _is_compatible(arch, glibc_version):
yield f"{legacy_tag}_{arch}"

View File

@@ -8,7 +8,7 @@ import functools
import re
import subprocess
import sys
from typing import Iterator, NamedTuple, Optional
from typing import Iterator, NamedTuple, Optional, Sequence
from ._elffile import ELFFile
@@ -28,7 +28,7 @@ def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
@functools.lru_cache()
@functools.lru_cache
def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
"""Detect currently-running musl runtime version.
@@ -47,24 +47,27 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
return None
if ld is None or "musl" not in ld:
return None
proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True)
return _parse_musl_version(proc.stderr)
def platform_tags(arch: str) -> Iterator[str]:
def platform_tags(archs: Sequence[str]) -> Iterator[str]:
"""Generate musllinux tags compatible to the current platform.
:param arch: Should be the part of platform tag after the ``linux_``
prefix, e.g. ``x86_64``. The ``linux_`` prefix is assumed as a
prerequisite for the current platform to be musllinux-compatible.
:param archs: Sequence of compatible architectures.
The first one shall be the closest to the actual architecture and be the part of
platform tag after the ``linux_`` prefix, e.g. ``x86_64``.
The ``linux_`` prefix is assumed as a prerequisite for the current platform to
be musllinux-compatible.
:returns: An iterator of compatible musllinux tags.
"""
sys_musl = _get_musl_version(sys.executable)
if sys_musl is None: # Python not dynamically linked against musl.
return
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
for arch in archs:
for minor in range(sys_musl.minor, -1, -1):
yield f"musllinux_{sys_musl.major}_{minor}_{arch}"
if __name__ == "__main__": # pragma: no cover

View File

@@ -1,6 +1,6 @@
"""Handwritten parser of dependency specifiers.
The docstring for each __parse_* function contains ENBF-inspired grammar representing
The docstring for each __parse_* function contains EBNF-inspired grammar representing
the implementation.
"""
@@ -163,7 +163,11 @@ def _parse_extras(tokenizer: Tokenizer) -> List[str]:
if not tokenizer.check("LEFT_BRACKET", peek=True):
return []
with tokenizer.enclosing_tokens("LEFT_BRACKET", "RIGHT_BRACKET"):
with tokenizer.enclosing_tokens(
"LEFT_BRACKET",
"RIGHT_BRACKET",
around="extras",
):
tokenizer.consume("WS")
extras = _parse_extras_list(tokenizer)
tokenizer.consume("WS")
@@ -203,7 +207,11 @@ def _parse_specifier(tokenizer: Tokenizer) -> str:
specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS
| WS? version_many WS?
"""
with tokenizer.enclosing_tokens("LEFT_PARENTHESIS", "RIGHT_PARENTHESIS"):
with tokenizer.enclosing_tokens(
"LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS",
around="version specifier",
):
tokenizer.consume("WS")
parsed_specifiers = _parse_version_many(tokenizer)
tokenizer.consume("WS")
@@ -217,7 +225,20 @@ def _parse_version_many(tokenizer: Tokenizer) -> str:
"""
parsed_specifiers = ""
while tokenizer.check("SPECIFIER"):
span_start = tokenizer.position
parsed_specifiers += tokenizer.read().text
if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
tokenizer.raise_syntax_error(
".* suffix can only be used with `==` or `!=` operators",
span_start=span_start,
span_end=tokenizer.position + 1,
)
if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True):
tokenizer.raise_syntax_error(
"Local version label can only be used with `==` or `!=` operators",
span_start=span_start,
span_end=tokenizer.position,
)
tokenizer.consume("WS")
if not tokenizer.check("COMMA"):
break
@@ -231,7 +252,13 @@ def _parse_version_many(tokenizer: Tokenizer) -> str:
# Recursive descent parser for marker expression
# --------------------------------------------------------------------------------------
def parse_marker(source: str) -> MarkerList:
return _parse_marker(Tokenizer(source, rules=DEFAULT_RULES))
return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES))
def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList:
retval = _parse_marker(tokenizer)
tokenizer.expect("END", expected="end of marker expression")
return retval
def _parse_marker(tokenizer: Tokenizer) -> MarkerList:
@@ -254,7 +281,11 @@ def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom:
tokenizer.consume("WS")
if tokenizer.check("LEFT_PARENTHESIS", peek=True):
with tokenizer.enclosing_tokens("LEFT_PARENTHESIS", "RIGHT_PARENTHESIS"):
with tokenizer.enclosing_tokens(
"LEFT_PARENTHESIS",
"RIGHT_PARENTHESIS",
around="marker expression",
):
tokenizer.consume("WS")
marker: MarkerAtom = _parse_marker(tokenizer)
tokenizer.consume("WS")
@@ -293,10 +324,7 @@ def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar:
def process_env_var(env_var: str) -> Variable:
if (
env_var == "platform_python_implementation"
or env_var == "python_implementation"
):
if env_var in ("platform_python_implementation", "python_implementation"):
return Variable("platform_python_implementation")
else:
return Variable(env_var)

View File

@@ -78,6 +78,8 @@ DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = {
"AT": r"\@",
"URL": r"[^ \t]+",
"IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b",
"VERSION_PREFIX_TRAIL": r"\.\*",
"VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*",
"WS": r"[ \t]+",
"END": r"$",
}
@@ -167,21 +169,23 @@ class Tokenizer:
)
@contextlib.contextmanager
def enclosing_tokens(self, open_token: str, close_token: str) -> Iterator[bool]:
def enclosing_tokens(
self, open_token: str, close_token: str, *, around: str
) -> Iterator[None]:
if self.check(open_token):
open_position = self.position
self.read()
else:
open_position = None
yield open_position is not None
yield
if open_position is None:
return
if not self.check(close_token):
self.raise_syntax_error(
f"Expected closing {close_token}",
f"Expected matching {close_token} for {open_token}, after {around}",
span_start=open_position,
)

View File

@@ -8,7 +8,16 @@ import platform
import sys
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable, parse_marker
from ._parser import (
MarkerAtom,
MarkerList,
Op,
Value,
Variable,
)
from ._parser import (
parse_marker as _parse_marker,
)
from ._tokenizer import ParserSyntaxError
from .specifiers import InvalidSpecifier, Specifier
from .utils import canonicalize_name
@@ -62,7 +71,6 @@ def _normalize_extra_values(results: Any) -> Any:
def _format_marker(
marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True
) -> str:
assert isinstance(marker, (list, tuple, str))
# Sometimes we have a structure like [[...]] which is a single item list
@@ -189,7 +197,7 @@ class Marker:
# packaging.requirements.Requirement. If any additional logic is
# added here, make sure to mirror/adapt Requirement.
try:
self._markers = _normalize_extra_values(parse_marker(marker))
self._markers = _normalize_extra_values(_parse_marker(marker))
# The attribute `_markers` can be described in terms of a recursive type:
# MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
#

View File

@@ -2,13 +2,13 @@
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
import urllib.parse
from typing import Any, List, Optional, Set
from typing import Any, Iterator, Optional, Set
from ._parser import parse_requirement
from ._parser import parse_requirement as _parse_requirement
from ._tokenizer import ParserSyntaxError
from .markers import Marker, _normalize_extra_values
from .specifiers import SpecifierSet
from .utils import canonicalize_name
class InvalidRequirement(ValueError):
@@ -32,62 +32,57 @@ class Requirement:
def __init__(self, requirement_string: str) -> None:
try:
parsed = parse_requirement(requirement_string)
parsed = _parse_requirement(requirement_string)
except ParserSyntaxError as e:
raise InvalidRequirement(str(e)) from e
self.name: str = parsed.name
if parsed.url:
parsed_url = urllib.parse.urlparse(parsed.url)
if parsed_url.scheme == "file":
if urllib.parse.urlunparse(parsed_url) != parsed.url:
raise InvalidRequirement("Invalid URL given")
elif not (parsed_url.scheme and parsed_url.netloc) or (
not parsed_url.scheme and not parsed_url.netloc
):
raise InvalidRequirement(f"Invalid URL: {parsed.url}")
self.url: Optional[str] = parsed.url
else:
self.url = None
self.extras: Set[str] = set(parsed.extras if parsed.extras else [])
self.url: Optional[str] = parsed.url or None
self.extras: Set[str] = set(parsed.extras or [])
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
self.marker: Optional[Marker] = None
if parsed.marker is not None:
self.marker = Marker.__new__(Marker)
self.marker._markers = _normalize_extra_values(parsed.marker)
def __str__(self) -> str:
parts: List[str] = [self.name]
def _iter_parts(self, name: str) -> Iterator[str]:
yield name
if self.extras:
formatted_extras = ",".join(sorted(self.extras))
parts.append(f"[{formatted_extras}]")
yield f"[{formatted_extras}]"
if self.specifier:
parts.append(str(self.specifier))
yield str(self.specifier)
if self.url:
parts.append(f"@ {self.url}")
yield f"@ {self.url}"
if self.marker:
parts.append(" ")
yield " "
if self.marker:
parts.append(f"; {self.marker}")
yield f"; {self.marker}"
return "".join(parts)
def __str__(self) -> str:
return "".join(self._iter_parts(self.name))
def __repr__(self) -> str:
return f"<Requirement('{self}')>"
def __hash__(self) -> int:
return hash((self.__class__.__name__, str(self)))
return hash(
(
self.__class__.__name__,
*self._iter_parts(canonicalize_name(self.name)),
)
)
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Requirement):
return NotImplemented
return (
self.name == other.name
canonicalize_name(self.name) == canonicalize_name(other.name)
and self.extras == other.extras
and self.specifier == other.specifier
and self.url == other.url

View File

@@ -1,5 +1,4 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.
"""
@@ -12,17 +11,7 @@
import abc
import itertools
import re
from typing import (
Callable,
Iterable,
Iterator,
List,
Optional,
Set,
Tuple,
TypeVar,
Union,
)
from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union
from .utils import canonicalize_version
from .version import Version
@@ -253,7 +242,8 @@ class Specifier(BaseSpecifier):
# Store whether or not this Specifier should accept prereleases
self._prereleases = prereleases
@property
# https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515
@property # type: ignore[override]
def prereleases(self) -> bool:
# If there is an explicit prereleases set for this, then we'll just
# blindly use that.
@@ -374,7 +364,6 @@ class Specifier(BaseSpecifier):
return operator_callable
def _compare_compatible(self, prospective: Version, spec: str) -> bool:
# Compatible releases have an equivalent combination of >= and ==. That
# is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
# implement this in terms of the other specifiers instead of
@@ -383,7 +372,7 @@ class Specifier(BaseSpecifier):
# We want everything but the last item in the version, but we want to
# ignore suffix segments.
prefix = ".".join(
prefix = _version_join(
list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
)
@@ -395,20 +384,21 @@ class Specifier(BaseSpecifier):
)
def _compare_equal(self, prospective: Version, spec: str) -> bool:
# We need special logic to handle prefix matching
if spec.endswith(".*"):
# In the case of prefix matching we want to ignore local segment.
normalized_prospective = canonicalize_version(prospective.public)
normalized_prospective = canonicalize_version(
prospective.public, strip_trailing_zero=False
)
# Get the normalized version string ignoring the trailing .*
normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
# Split the spec out by dots, and pretend that there is an implicit
# dot in between a release segment and a pre-release segment.
# Split the spec out by bangs and dots, and pretend that there is
# an implicit dot in between a release segment and a pre-release segment.
split_spec = _version_split(normalized_spec)
# Split the prospective version out by dots, and pretend that there
# is an implicit dot in between a release segment and a pre-release
# segment.
# Split the prospective version out by bangs and dots, and pretend
# that there is an implicit dot in between a release segment and
# a pre-release segment.
split_prospective = _version_split(normalized_prospective)
# 0-pad the prospective version before shortening it to get the correct
@@ -437,21 +427,18 @@ class Specifier(BaseSpecifier):
return not self._compare_equal(prospective, spec)
def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
# NB: Local version identifiers are NOT permitted in the version
# specifier, so local version labels can be universally removed from
# the prospective version.
return Version(prospective.public) <= Version(spec)
def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
# NB: Local version identifiers are NOT permitted in the version
# specifier, so local version labels can be universally removed from
# the prospective version.
return Version(prospective.public) >= Version(spec)
def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
# Convert our spec to a Version instance, since we'll want to work with
# it as a version.
spec = Version(spec_str)
@@ -476,7 +463,6 @@ class Specifier(BaseSpecifier):
return True
def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
# Convert our spec to a Version instance, since we'll want to work with
# it as a version.
spec = Version(spec_str)
@@ -642,8 +628,19 @@ _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
def _version_split(version: str) -> List[str]:
"""Split version into components.
The split components are intended for version comparison. The logic does
not attempt to retain the original version string, so joining the
components back with :func:`_version_join` may not produce the original
version string.
"""
result: List[str] = []
for item in version.split("."):
epoch, _, rest = version.rpartition("!")
result.append(epoch or "0")
for item in rest.split("."):
match = _prefix_regex.search(item)
if match:
result.extend(match.groups())
@@ -652,6 +649,17 @@ def _version_split(version: str) -> List[str]:
return result
def _version_join(components: List[str]) -> str:
"""Join split version components into a version string.
This function assumes the input came from :func:`_version_split`, where the
first component must be the epoch (either empty or numeric), and all other
components numeric.
"""
epoch, *rest = components
return f"{epoch}!{'.'.join(rest)}"
def _is_not_suffix(segment: str) -> bool:
return not any(
segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
@@ -673,7 +681,10 @@ def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str
left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))
return (
list(itertools.chain.from_iterable(left_split)),
list(itertools.chain.from_iterable(right_split)),
)
class SpecifierSet(BaseSpecifier):
@@ -705,14 +716,8 @@ class SpecifierSet(BaseSpecifier):
# strip each item to remove leading/trailing whitespace.
split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
# Parsed each individual specifier, attempting first to make it a
# Specifier.
parsed: Set[Specifier] = set()
for specifier in split_specifiers:
parsed.add(Specifier(specifier))
# Turn our parsed specifiers into a frozen set and save them for later.
self._specs = frozenset(parsed)
# Make each individual specifier a Specifier and save in a frozen set for later.
self._specs = frozenset(map(Specifier, split_specifiers))
# Store our prereleases value so we can use it later to determine if
# we accept prereleases or not.

View File

@@ -4,6 +4,8 @@
import logging
import platform
import re
import struct
import subprocess
import sys
import sysconfig
@@ -37,7 +39,7 @@ INTERPRETER_SHORT_NAMES: Dict[str, str] = {
}
_32_BIT_INTERPRETER = sys.maxsize <= 2**32
_32_BIT_INTERPRETER = struct.calcsize("P") == 4
class Tag:
@@ -111,7 +113,7 @@ def parse_tag(tag: str) -> FrozenSet[Tag]:
def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
value = sysconfig.get_config_var(name)
value: Union[int, str, None] = sysconfig.get_config_var(name)
if value is None and warn:
logger.debug(
"Config variable '%s' is unset, Python ABI tag may be incorrect", name
@@ -120,23 +122,40 @@ def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
def _normalize_string(string: str) -> str:
return string.replace(".", "_").replace("-", "_")
return string.replace(".", "_").replace("-", "_").replace(" ", "_")
def _abi3_applies(python_version: PythonVersion) -> bool:
def _is_threaded_cpython(abis: List[str]) -> bool:
"""
Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
The threaded builds are indicated by a "t" in the abiflags.
"""
if len(abis) == 0:
return False
# expect e.g., cp313
m = re.match(r"cp\d+(.*)", abis[0])
if not m:
return False
abiflags = m.group(1)
return "t" in abiflags
def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
"""
Determine if the Python version supports abi3.
PEP 384 was first implemented in Python 3.2.
PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`)
builds do not support abi3.
"""
return len(python_version) > 1 and tuple(python_version) >= (3, 2)
return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
py_version = tuple(py_version) # To allow for version comparison.
abis = []
version = _version_nodot(py_version[:2])
debug = pymalloc = ucs4 = ""
threading = debug = pymalloc = ucs4 = ""
with_debug = _get_config_var("Py_DEBUG", warn)
has_refcount = hasattr(sys, "gettotalrefcount")
# Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
@@ -145,6 +164,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
has_ext = "_d.pyd" in EXTENSION_SUFFIXES
if with_debug or (with_debug is None and (has_refcount or has_ext)):
debug = "d"
if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn):
threading = "t"
if py_version < (3, 8):
with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
if with_pymalloc or with_pymalloc is None:
@@ -158,13 +179,8 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
elif debug:
# Debug builds can also load "normal" extension modules.
# We can also assume no UCS-4 or pymalloc requirement.
abis.append(f"cp{version}")
abis.insert(
0,
"cp{version}{debug}{pymalloc}{ucs4}".format(
version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4
),
)
abis.append(f"cp{version}{threading}")
abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}")
return abis
@@ -212,11 +228,14 @@ def cpython_tags(
for abi in abis:
for platform_ in platforms:
yield Tag(interpreter, abi, platform_)
if _abi3_applies(python_version):
threading = _is_threaded_cpython(abis)
use_abi3 = _abi3_applies(python_version, threading)
if use_abi3:
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
if _abi3_applies(python_version):
if use_abi3:
for minor_version in range(python_version[1] - 1, 1, -1):
for platform_ in platforms:
interpreter = "cp{version}".format(
@@ -406,7 +425,7 @@ def mac_platforms(
check=True,
env={"SYSTEM_VERSION_COMPAT": "0"},
stdout=subprocess.PIPE,
universal_newlines=True,
text=True,
).stdout
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
else:
@@ -469,15 +488,21 @@ def mac_platforms(
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
linux = _normalize_string(sysconfig.get_platform())
if not linux.startswith("linux_"):
# we should never be here, just yield the sysconfig one and return
yield linux
return
if is_32bit:
if linux == "linux_x86_64":
linux = "linux_i686"
elif linux == "linux_aarch64":
linux = "linux_armv7l"
linux = "linux_armv8l"
_, arch = linux.split("_", 1)
yield from _manylinux.platform_tags(linux, arch)
yield from _musllinux.platform_tags(arch)
yield linux
archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch])
yield from _manylinux.platform_tags(archs)
yield from _musllinux.platform_tags(archs)
for arch in archs:
yield f"linux_{arch}"
def _generic_platforms() -> Iterator[str]:

View File

@@ -12,6 +12,12 @@ BuildTag = Union[Tuple[()], Tuple[int, str]]
NormalizedName = NewType("NormalizedName", str)
class InvalidName(ValueError):
"""
An invalid distribution name; users should refer to the packaging user guide.
"""
class InvalidWheelFilename(ValueError):
"""
An invalid wheel filename was found, users should refer to PEP 427.
@@ -24,17 +30,28 @@ class InvalidSdistFilename(ValueError):
"""
# Core metadata spec for `Name`
_validate_regex = re.compile(
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
)
_canonicalize_regex = re.compile(r"[-_.]+")
_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$")
# PEP 427: The build number must start with a digit.
_build_tag_regex = re.compile(r"(\d+)(.*)")
def canonicalize_name(name: str) -> NormalizedName:
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
if validate and not _validate_regex.match(name):
raise InvalidName(f"name is invalid: {name!r}")
# This is taken from PEP 503.
value = _canonicalize_regex.sub("-", name).lower()
return cast(NormalizedName, value)
def is_normalized_name(name: str) -> bool:
return _normalized_regex.match(name) is not None
def canonicalize_version(
version: Union[Version, str], *, strip_trailing_zero: bool = True
) -> str:
@@ -100,11 +117,18 @@ def parse_wheel_filename(
parts = filename.split("-", dashes - 2)
name_part = parts[0]
# See PEP 427 for the rules on escaping the project name
# See PEP 427 for the rules on escaping the project name.
if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
raise InvalidWheelFilename(f"Invalid project name: {filename}")
name = canonicalize_name(name_part)
version = Version(parts[1])
try:
version = Version(parts[1])
except InvalidVersion as e:
raise InvalidWheelFilename(
f"Invalid wheel filename (invalid version): {filename}"
) from e
if dashes == 5:
build_part = parts[2]
build_match = _build_tag_regex.match(build_part)
@@ -137,5 +161,12 @@ def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
name = canonicalize_name(name_part)
version = Version(version_part)
try:
version = Version(version_part)
except InvalidVersion as e:
raise InvalidSdistFilename(
f"Invalid sdist filename (invalid version): {filename}"
) from e
return (name, version)

View File

@@ -7,37 +7,39 @@
from packaging.version import parse, Version
"""
import collections
import itertools
import re
from typing import Callable, Optional, SupportsInt, Tuple, Union
from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]
InfiniteTypes = Union[InfinityType, NegativeInfinityType]
PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
SubLocalType = Union[InfiniteTypes, int, str]
LocalType = Union[
LocalType = Tuple[Union[int, str], ...]
CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]]
CmpLocalType = Union[
NegativeInfinityType,
Tuple[
Union[
SubLocalType,
Tuple[SubLocalType, str],
Tuple[NegativeInfinityType, SubLocalType],
],
...,
],
Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...],
]
CmpKey = Tuple[
int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
int,
Tuple[int, ...],
CmpPrePostDevType,
CmpPrePostDevType,
CmpPrePostDevType,
CmpLocalType,
]
VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
_Version = collections.namedtuple(
"_Version", ["epoch", "release", "dev", "pre", "post", "local"]
)
class _Version(NamedTuple):
epoch: int
release: Tuple[int, ...]
dev: Optional[Tuple[str, int]]
pre: Optional[Tuple[str, int]]
post: Optional[Tuple[str, int]]
local: Optional[LocalType]
def parse(version: str) -> "Version":
@@ -63,7 +65,7 @@ class InvalidVersion(ValueError):
class _BaseVersion:
_key: CmpKey
_key: Tuple[Any, ...]
def __hash__(self) -> int:
return hash(self._key)
@@ -117,7 +119,7 @@ _VERSION_PATTERN = r"""
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
(?P<pre_l>alpha|a|beta|b|preview|pre|c|rc)
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
@@ -179,6 +181,7 @@ class Version(_BaseVersion):
"""
_regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
_key: CmpKey
def __init__(self, version: str) -> None:
"""Initialize a Version object.
@@ -268,8 +271,7 @@ class Version(_BaseVersion):
>>> Version("1!2.0.0").epoch
1
"""
_epoch: int = self._version.epoch
return _epoch
return self._version.epoch
@property
def release(self) -> Tuple[int, ...]:
@@ -285,8 +287,7 @@ class Version(_BaseVersion):
Includes trailing zeroes but not the epoch or any pre-release / development /
post-release suffixes.
"""
_release: Tuple[int, ...] = self._version.release
return _release
return self._version.release
@property
def pre(self) -> Optional[Tuple[str, int]]:
@@ -301,8 +302,7 @@ class Version(_BaseVersion):
>>> Version("1.2.3rc1").pre
('rc', 1)
"""
_pre: Optional[Tuple[str, int]] = self._version.pre
return _pre
return self._version.pre
@property
def post(self) -> Optional[int]:
@@ -450,9 +450,8 @@ class Version(_BaseVersion):
def _parse_letter_version(
letter: str, number: Union[str, bytes, SupportsInt]
letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
) -> Optional[Tuple[str, int]]:
if letter:
# We consider there to be an implicit 0 in a pre-release if there is
# not a numeral associated with it.
@@ -488,7 +487,7 @@ def _parse_letter_version(
_local_version_separators = re.compile(r"[\._-]")
def _parse_local_version(local: str) -> Optional[LocalType]:
def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
"""
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
"""
@@ -506,9 +505,8 @@ def _cmpkey(
pre: Optional[Tuple[str, int]],
post: Optional[Tuple[str, int]],
dev: Optional[Tuple[str, int]],
local: Optional[Tuple[SubLocalType]],
local: Optional[LocalType],
) -> CmpKey:
# When we compare a release version, we want to compare it with all of the
# trailing zeros removed. So we'll use a reverse the list, drop all the now
# leading zeros until we come to something non zero, then take the rest
@@ -523,7 +521,7 @@ def _cmpkey(
# if there is not a pre or a post segment. If we have one of those then
# the normal sorting rules will handle this case correctly.
if pre is None and post is None and dev is not None:
_pre: PrePostDevType = NegativeInfinity
_pre: CmpPrePostDevType = NegativeInfinity
# Versions without a pre-release (except as noted above) should sort after
# those with one.
elif pre is None:
@@ -533,21 +531,21 @@ def _cmpkey(
# Versions without a post segment should sort before those with one.
if post is None:
_post: PrePostDevType = NegativeInfinity
_post: CmpPrePostDevType = NegativeInfinity
else:
_post = post
# Versions without a development segment should sort after those with one.
if dev is None:
_dev: PrePostDevType = Infinity
_dev: CmpPrePostDevType = Infinity
else:
_dev = dev
if local is None:
# Versions without a local segment should sort before those with one.
_local: LocalType = NegativeInfinity
_local: CmpLocalType = NegativeInfinity
else:
# Versions with a local segment need that segment parsed to implement
# the sorting rules in PEP440.

View File

@@ -1 +1 @@
packaging==23.0
packaging==24.0

View File

@@ -7,11 +7,22 @@ import re
import stat
import time
from io import StringIO, TextIOWrapper
from typing import IO, TYPE_CHECKING, Literal
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from wheel.cli import WheelError
from wheel.util import log, urlsafe_b64decode, urlsafe_b64encode
if TYPE_CHECKING:
from typing import Protocol, Sized, Union
from typing_extensions import Buffer
StrPath = Union[str, os.PathLike[str]]
class SizedBuffer(Sized, Buffer, Protocol): ...
# Non-greedy matching of an optional build number may be too clever (more
# invalid wheel filenames will match). Separate regex for .dist-info?
WHEEL_INFO_RE = re.compile(
@@ -22,7 +33,7 @@ WHEEL_INFO_RE = re.compile(
MINIMUM_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC
def get_zipinfo_datetime(timestamp=None):
def get_zipinfo_datetime(timestamp: float | None = None):
# Some applications need reproducible .whl files, but they can't do this without
# forcing the timestamp of the individual ZipInfo objects. See issue #143.
timestamp = int(os.environ.get("SOURCE_DATE_EPOCH", timestamp or time.time()))
@@ -37,7 +48,12 @@ class WheelFile(ZipFile):
_default_algorithm = hashlib.sha256
def __init__(self, file, mode="r", compression=ZIP_DEFLATED):
def __init__(
self,
file: StrPath,
mode: Literal["r", "w", "x", "a"] = "r",
compression: int = ZIP_DEFLATED,
):
basename = os.path.basename(file)
self.parsed_filename = WHEEL_INFO_RE.match(basename)
if not basename.endswith(".whl") or self.parsed_filename is None:
@@ -49,7 +65,7 @@ class WheelFile(ZipFile):
self.parsed_filename.group("namever")
)
self.record_path = self.dist_info_path + "/RECORD"
self._file_hashes = {}
self._file_hashes: dict[str, tuple[None, None] | tuple[int, bytes]] = {}
self._file_sizes = {}
if mode == "r":
# Ignore RECORD and any embedded wheel signatures
@@ -81,8 +97,8 @@ class WheelFile(ZipFile):
if algorithm.lower() in {"md5", "sha1"}:
raise WheelError(
"Weak hash algorithm ({}) is not permitted by PEP "
"427".format(algorithm)
f"Weak hash algorithm ({algorithm}) is not permitted by "
f"PEP 427"
)
self._file_hashes[path] = (
@@ -90,8 +106,13 @@ class WheelFile(ZipFile):
urlsafe_b64decode(hash_sum.encode("ascii")),
)
def open(self, name_or_info, mode="r", pwd=None):
def _update_crc(newdata):
def open(
self,
name_or_info: str | ZipInfo,
mode: Literal["r", "w"] = "r",
pwd: bytes | None = None,
) -> IO[bytes]:
def _update_crc(newdata: bytes) -> None:
eof = ef._eof
update_crc_orig(newdata)
running_hash.update(newdata)
@@ -119,9 +140,9 @@ class WheelFile(ZipFile):
return ef
def write_files(self, base_dir):
def write_files(self, base_dir: str):
log.info(f"creating '{self.filename}' and adding '{base_dir}' to it")
deferred = []
deferred: list[tuple[str, str]] = []
for root, dirnames, filenames in os.walk(base_dir):
# Sort the directory names so that `os.walk` will walk them in a
# defined order on the next iteration.
@@ -141,7 +162,12 @@ class WheelFile(ZipFile):
for path, arcname in deferred:
self.write(path, arcname)
def write(self, filename, arcname=None, compress_type=None):
def write(
self,
filename: str,
arcname: str | None = None,
compress_type: int | None = None,
) -> None:
with open(filename, "rb") as f:
st = os.fstat(f.fileno())
data = f.read()
@@ -153,7 +179,12 @@ class WheelFile(ZipFile):
zinfo.compress_type = compress_type or self.compression
self.writestr(zinfo, data, compress_type)
def writestr(self, zinfo_or_arcname, data, compress_type=None):
def writestr(
self,
zinfo_or_arcname: str | ZipInfo,
data: SizedBuffer | str,
compress_type: int | None = None,
):
if isinstance(zinfo_or_arcname, str):
zinfo_or_arcname = ZipInfo(
zinfo_or_arcname, date_time=get_zipinfo_datetime()