diff --git a/src/wheel/_setuptools_logging.py b/src/wheel/_setuptools_logging.py index 006c0985..a1a2482b 100644 --- a/src/wheel/_setuptools_logging.py +++ b/src/wheel/_setuptools_logging.py @@ -5,11 +5,11 @@ 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 diff --git a/src/wheel/bdist_wheel.py b/src/wheel/bdist_wheel.py index 6b811ee3..4a6f0371 100644 --- a/src/wheel/bdist_wheel.py +++ b/src/wheel/bdist_wheel.py @@ -18,6 +18,7 @@ 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 @@ -31,15 +32,18 @@ from .vendored.packaging import version as _packaging_version from .wheelfile import WheelFile +if TYPE_CHECKING: + import types -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 '-'. """ return re.sub("[^A-Za-z0-9.]+", "-", name) -def safe_version(version): +def safe_version(version: str) -> str: """ Convert an arbitrary string to a standard version string """ @@ -56,15 +60,15 @@ def safe_version(version): PY_LIMITED_API_PATTERN = r"cp3\d" -def _is_32bit_interpreter(): +def _is_32bit_interpreter() -> bool: return struct.calcsize("P") == 4 -def python_tag(): +def python_tag() -> str: return f"py{sys.version_info[0]}" -def get_platform(archive_root): +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: @@ -82,7 +86,9 @@ def get_platform(archive_root): return result.replace("-", "_") -def get_flag(var, fallback, expected=True, warn=True): +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) @@ -97,9 +103,9 @@ def get_flag(var, fallback, expected=True, warn=True): return val == expected -def get_abi_tag(): +def get_abi_tag() -> str | None: """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2).""" - soabi = sysconfig.get_config_var("SOABI") + soabi: str = sysconfig.get_config_var("SOABI") impl = tags.interpreter_name() if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"): d = "" @@ -137,19 +143,23 @@ def get_abi_tag(): return abi -def safer_name(name): +def safer_name(name: str) -> str: return safe_name(name).replace("-", "_") -def safer_version(version): +def safer_version(version: str) -> str: return safe_version(version).replace("-", "_") -def remove_readonly(func, path, excinfo): +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, path, exc): +def remove_readonly_exc(func: Callable[..., object], path: str, exc: Exception) -> None: os.chmod(path, stat.S_IWRITE) func(path) @@ -224,24 +234,24 @@ class bdist_wheel(Command): boolean_options = ["keep-temp", "skip-build", "relative", "universal"] def initialize_options(self): - self.bdist_dir = None + self.bdist_dir: str = None self.data_dir = None - self.plat_name = None + self.plat_name: str | None = None self.plat_tag = None self.format = "zip" self.keep_temp = False - self.dist_dir = None + self.dist_dir: str | None = None self.egginfo_dir = None - self.root_is_pure = None + self.root_is_pure: bool | None = 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.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): @@ -298,11 +308,11 @@ def wheel_dist_name(self): components += (self.build_number,) return "-".join(components) - def get_tag(self): + 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 = self.plat_name + plat_name = cast(str, self.plat_name) elif self.root_is_pure: plat_name = "any" else: @@ -447,7 +457,7 @@ def run(self): rmtree(self.bdist_dir, onexc=remove_readonly_exc) def write_wheelfile( - self, wheelfile_base, generator="bdist_wheel (" + wheel_version + ")" + self, wheelfile_base: str, generator: str = f"bdist_wheel ({wheel_version})" ): from email.message import Message @@ -470,7 +480,7 @@ def write_wheelfile( with open(wheelfile_path, "wb") as f: BytesGenerator(f, maxheaderlen=0).flatten(msg) - def _ensure_relative(self, path): + def _ensure_relative(self, path: str) -> str: # copied from dir_util, deleted drive, path = os.path.splitdrive(path) if path[0:1] == os.sep: @@ -478,16 +488,16 @@ def _ensure_relative(self, path): return path @property - def license_paths(self): + 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() + 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 = self.distribution.metadata.license_files + 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: @@ -522,10 +532,10 @@ def license_paths(self): return files - def egg2dist(self, egginfo_path, distinfo_path): + def egg2dist(self, egginfo_path: str, distinfo_path: str): """Convert an .egg-info directory into a .dist-info directory""" - def adios(p): + 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) diff --git a/src/wheel/cli/__init__.py b/src/wheel/cli/__init__.py index a38860f5..a828caad 100644 --- a/src/wheel/cli/__init__.py +++ b/src/wheel/cli/__init__.py @@ -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,7 +51,7 @@ def tags_f(args): print(name) -def version_f(args): +def version_f(args: argparse.Namespace) -> None: from .. import __version__ print("wheel %s" % __version__) diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index 29153404..4c23560d 100644 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -96,7 +96,7 @@ def egg2wheel(egg_path: str, dest_dir: str) -> None: 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 @@ -193,7 +193,7 @@ def wininst2wheel(path, dest_dir): # 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) @@ -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": diff --git a/src/wheel/macosx_libfile.py b/src/wheel/macosx_libfile.py index 8953c3f8..abdfc9ed 100644 --- a/src/wheel/macosx_libfile.py +++ b/src/wheel/macosx_libfile.py @@ -43,6 +43,13 @@ 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 @@ """ -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 @@ class FatArch(BaseClass): 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,7 +343,10 @@ class FatArch(BaseClass): 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 function parses a Mach-O header and extracts information about the minimal macOS version. @@ -380,14 +393,14 @@ class VersionBuild(base_class): 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 @@ -420,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"): diff --git a/src/wheel/metadata.py b/src/wheel/metadata.py index 6aa43628..ee44c07e 100644 --- a/src/wheel/metadata.py +++ b/src/wheel/metadata.py @@ -11,17 +11,17 @@ 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('')) @@ -39,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 @@ -51,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("]"): @@ -68,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. @@ -76,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 '-'. """ @@ -85,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) @@ -111,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 diff --git a/src/wheel/wheelfile.py b/src/wheel/wheelfile.py index 6440e90a..0a0f4596 100644 --- a/src/wheel/wheelfile.py +++ b/src/wheel/wheelfile.py @@ -7,11 +7,22 @@ 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 @@ 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 @@ def __init__(self, file, mode="r", compression=ZIP_DEFLATED): 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 @@ -90,8 +106,13 @@ def __init__(self, file, mode="r", compression=ZIP_DEFLATED): 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 @@ def _update_crc(newdata): 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 @@ def write_files(self, base_dir): 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 @@ def write(self, filename, arcname=None, compress_type=None): 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()