Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
153
.venv/lib/python3.11/site-packages/wheel/_commands/__init__.py
Normal file
153
.venv/lib/python3.11/site-packages/wheel/_commands/__init__.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
Wheel command-line utility.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentTypeError
|
||||
|
||||
from ..wheelfile import WheelError
|
||||
|
||||
|
||||
def unpack_f(args: argparse.Namespace) -> None:
|
||||
from .unpack import unpack
|
||||
|
||||
unpack(args.wheelfile, args.dest)
|
||||
|
||||
|
||||
def pack_f(args: argparse.Namespace) -> None:
|
||||
from .pack import pack
|
||||
|
||||
pack(args.directory, args.dest_dir, args.build_number)
|
||||
|
||||
|
||||
def convert_f(args: argparse.Namespace) -> None:
|
||||
from .convert import convert
|
||||
|
||||
convert(args.files, args.dest_dir, args.verbose)
|
||||
|
||||
|
||||
def tags_f(args: argparse.Namespace) -> None:
|
||||
from .tags import tags
|
||||
|
||||
names = (
|
||||
tags(
|
||||
wheel,
|
||||
args.python_tag,
|
||||
args.abi_tag,
|
||||
args.platform_tag,
|
||||
args.build,
|
||||
args.remove,
|
||||
)
|
||||
for wheel in args.wheel
|
||||
)
|
||||
|
||||
for name in names:
|
||||
print(name)
|
||||
|
||||
|
||||
def version_f(args: argparse.Namespace) -> None:
|
||||
from .. import __version__
|
||||
|
||||
print(f"wheel {__version__}")
|
||||
|
||||
|
||||
def parse_build_tag(build_tag: str) -> str:
|
||||
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")
|
||||
|
||||
return build_tag
|
||||
|
||||
|
||||
TAGS_HELP = """\
|
||||
Make a new wheel with given tags. Any tags unspecified will remain the same.
|
||||
Starting the tags with a "+" will append to the existing tags. Starting with a
|
||||
"-" will remove a tag (use --option=-TAG syntax). Multiple tags can be
|
||||
separated by ".". The original file will remain unless --remove is given. The
|
||||
output filename(s) will be displayed on stdout for further processing.
|
||||
"""
|
||||
|
||||
|
||||
def parser() -> argparse.ArgumentParser:
|
||||
p = argparse.ArgumentParser()
|
||||
s = p.add_subparsers(help="commands")
|
||||
|
||||
unpack_parser = s.add_parser("unpack", help="Unpack wheel")
|
||||
unpack_parser.add_argument(
|
||||
"--dest", "-d", help="Destination directory", default="."
|
||||
)
|
||||
unpack_parser.add_argument("wheelfile", help="Wheel file")
|
||||
unpack_parser.set_defaults(func=unpack_f)
|
||||
|
||||
repack_parser = s.add_parser("pack", help="Repack wheel")
|
||||
repack_parser.add_argument("directory", help="Root directory of the unpacked wheel")
|
||||
repack_parser.add_argument(
|
||||
"--dest-dir",
|
||||
"-d",
|
||||
default=os.path.curdir,
|
||||
help="Directory to store the wheel (default %(default)s)",
|
||||
)
|
||||
repack_parser.add_argument(
|
||||
"--build-number", help="Build tag to use in the wheel name"
|
||||
)
|
||||
repack_parser.set_defaults(func=pack_f)
|
||||
|
||||
convert_parser = s.add_parser("convert", help="Convert egg or wininst to wheel")
|
||||
convert_parser.add_argument("files", nargs="*", help="Files to convert")
|
||||
convert_parser.add_argument(
|
||||
"--dest-dir",
|
||||
"-d",
|
||||
default=os.path.curdir,
|
||||
help="Directory to store wheels (default %(default)s)",
|
||||
)
|
||||
convert_parser.add_argument("--verbose", "-v", action="store_true")
|
||||
convert_parser.set_defaults(func=convert_f)
|
||||
|
||||
tags_parser = s.add_parser(
|
||||
"tags", help="Add or replace the tags on a wheel", description=TAGS_HELP
|
||||
)
|
||||
tags_parser.add_argument("wheel", nargs="*", help="Existing wheel(s) to retag")
|
||||
tags_parser.add_argument(
|
||||
"--remove",
|
||||
action="store_true",
|
||||
help="Remove the original files, keeping only the renamed ones",
|
||||
)
|
||||
tags_parser.add_argument(
|
||||
"--python-tag", metavar="TAG", help="Specify an interpreter tag(s)"
|
||||
)
|
||||
tags_parser.add_argument("--abi-tag", metavar="TAG", help="Specify an ABI tag(s)")
|
||||
tags_parser.add_argument(
|
||||
"--platform-tag", metavar="TAG", help="Specify a platform tag(s)"
|
||||
)
|
||||
tags_parser.add_argument(
|
||||
"--build", type=parse_build_tag, metavar="BUILD", help="Specify a build tag"
|
||||
)
|
||||
tags_parser.set_defaults(func=tags_f)
|
||||
|
||||
version_parser = s.add_parser("version", help="Print version and exit")
|
||||
version_parser.set_defaults(func=version_f)
|
||||
|
||||
help_parser = s.add_parser("help", help="Show this help")
|
||||
help_parser.set_defaults(func=lambda args: p.print_help())
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = parser()
|
||||
args = p.parse_args()
|
||||
if not hasattr(args, "func"):
|
||||
p.print_help()
|
||||
else:
|
||||
try:
|
||||
args.func(args)
|
||||
return 0
|
||||
except WheelError as e:
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
return 1
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
337
.venv/lib/python3.11/site-packages/wheel/_commands/convert.py
Normal file
337
.venv/lib/python3.11/site-packages/wheel/_commands/convert.py
Normal file
@@ -0,0 +1,337 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
import re
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterator
|
||||
from email.message import Message
|
||||
from email.parser import Parser
|
||||
from email.policy import EmailPolicy
|
||||
from glob import iglob
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from zipfile import ZipFile
|
||||
|
||||
from packaging.tags import parse_tag
|
||||
|
||||
from .. import __version__
|
||||
from .._metadata import generate_requirements
|
||||
from ..wheelfile import WheelFile
|
||||
|
||||
egg_filename_re = re.compile(
|
||||
r"""
|
||||
(?P<name>.+?)-(?P<ver>.+?)
|
||||
(-(?P<pyver>py\d\.\d+)
|
||||
(-(?P<arch>.+?))?
|
||||
)?.egg$""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
egg_info_re = re.compile(
|
||||
r"""
|
||||
^(?P<name>.+?)-(?P<ver>.+?)
|
||||
(-(?P<pyver>py\d\.\d+)
|
||||
)?.egg-info/""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
wininst_re = re.compile(
|
||||
r"\.(?P<platform>win32|win-amd64)(?:-(?P<pyver>py\d\.\d))?\.exe$"
|
||||
)
|
||||
pyd_re = re.compile(r"\.(?P<abi>[a-z0-9]+)-(?P<platform>win32|win_amd64)\.pyd$")
|
||||
serialization_policy = EmailPolicy(
|
||||
utf8=True,
|
||||
mangle_from_=False,
|
||||
max_line_length=0,
|
||||
)
|
||||
GENERATOR = f"wheel {__version__}"
|
||||
|
||||
|
||||
def convert_requires(requires: str, metadata: Message) -> None:
|
||||
extra: str | None = None
|
||||
requirements: dict[str | None, list[str]] = defaultdict(list)
|
||||
for line in requires.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
extra = line[1:-1]
|
||||
continue
|
||||
|
||||
requirements[extra].append(line)
|
||||
|
||||
for key, value in generate_requirements(requirements):
|
||||
metadata.add_header(key, value)
|
||||
|
||||
|
||||
def convert_pkg_info(pkginfo: str, metadata: Message) -> None:
|
||||
parsed_message = Parser().parsestr(pkginfo)
|
||||
for key, value in parsed_message.items():
|
||||
key_lower = key.lower()
|
||||
if value == "UNKNOWN":
|
||||
continue
|
||||
|
||||
if key_lower == "description":
|
||||
description_lines = value.splitlines()
|
||||
if description_lines:
|
||||
value = "\n".join(
|
||||
(
|
||||
description_lines[0].lstrip(),
|
||||
dedent("\n".join(description_lines[1:])),
|
||||
"\n",
|
||||
)
|
||||
)
|
||||
else:
|
||||
value = "\n"
|
||||
|
||||
metadata.set_payload(value)
|
||||
elif key_lower == "home-page":
|
||||
metadata.add_header("Project-URL", f"Homepage, {value}")
|
||||
elif key_lower == "download-url":
|
||||
metadata.add_header("Project-URL", f"Download, {value}")
|
||||
else:
|
||||
metadata.add_header(key, value)
|
||||
|
||||
metadata.replace_header("Metadata-Version", "2.4")
|
||||
|
||||
|
||||
def normalize(name: str) -> str:
|
||||
return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_")
|
||||
|
||||
|
||||
class ConvertSource(metaclass=ABCMeta):
|
||||
name: str
|
||||
version: str
|
||||
pyver: str = "py2.py3"
|
||||
abi: str = "none"
|
||||
platform: str = "any"
|
||||
metadata: Message
|
||||
|
||||
@property
|
||||
def dist_info_dir(self) -> str:
|
||||
return f"{self.name}-{self.version}.dist-info"
|
||||
|
||||
@abstractmethod
|
||||
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
|
||||
pass
|
||||
|
||||
|
||||
class EggFileSource(ConvertSource):
|
||||
def __init__(self, path: Path):
|
||||
if not (match := egg_filename_re.match(path.name)):
|
||||
raise ValueError(f"Invalid egg file name: {path.name}")
|
||||
|
||||
# Binary wheels are assumed to be for CPython
|
||||
self.path = path
|
||||
self.name = normalize(match.group("name"))
|
||||
self.version = match.group("ver")
|
||||
if pyver := match.group("pyver"):
|
||||
self.pyver = pyver.replace(".", "")
|
||||
if arch := match.group("arch"):
|
||||
self.abi = self.pyver.replace("py", "cp")
|
||||
self.platform = normalize(arch)
|
||||
|
||||
self.metadata = Message()
|
||||
|
||||
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
|
||||
with ZipFile(self.path, "r") as zip_file:
|
||||
for filename in sorted(zip_file.namelist()):
|
||||
# Skip pure directory entries
|
||||
if filename.endswith("/"):
|
||||
continue
|
||||
|
||||
# Handle files in the egg-info directory specially, selectively moving
|
||||
# them to the dist-info directory while converting as needed
|
||||
if filename.startswith("EGG-INFO/"):
|
||||
if filename == "EGG-INFO/requires.txt":
|
||||
requires = zip_file.read(filename).decode("utf-8")
|
||||
convert_requires(requires, self.metadata)
|
||||
elif filename == "EGG-INFO/PKG-INFO":
|
||||
pkginfo = zip_file.read(filename).decode("utf-8")
|
||||
convert_pkg_info(pkginfo, self.metadata)
|
||||
elif filename == "EGG-INFO/entry_points.txt":
|
||||
yield (
|
||||
f"{self.dist_info_dir}/entry_points.txt",
|
||||
zip_file.read(filename),
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
# For any other file, just pass it through
|
||||
yield filename, zip_file.read(filename)
|
||||
|
||||
|
||||
class EggDirectorySource(EggFileSource):
|
||||
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
|
||||
for dirpath, _, filenames in os.walk(self.path):
|
||||
for filename in sorted(filenames):
|
||||
path = Path(dirpath, filename)
|
||||
if path.parent.name == "EGG-INFO":
|
||||
if path.name == "requires.txt":
|
||||
requires = path.read_text("utf-8")
|
||||
convert_requires(requires, self.metadata)
|
||||
elif path.name == "PKG-INFO":
|
||||
pkginfo = path.read_text("utf-8")
|
||||
convert_pkg_info(pkginfo, self.metadata)
|
||||
if name := self.metadata.get("Name"):
|
||||
self.name = normalize(name)
|
||||
|
||||
if version := self.metadata.get("Version"):
|
||||
self.version = version
|
||||
elif path.name == "entry_points.txt":
|
||||
yield (
|
||||
f"{self.dist_info_dir}/entry_points.txt",
|
||||
path.read_bytes(),
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
# For any other file, just pass it through
|
||||
yield str(path.relative_to(self.path)), path.read_bytes()
|
||||
|
||||
|
||||
class WininstFileSource(ConvertSource):
|
||||
"""
|
||||
Handles distributions created with ``bdist_wininst``.
|
||||
|
||||
The egginfo filename has the format::
|
||||
|
||||
name-ver(-pyver)(-arch).egg-info
|
||||
|
||||
The installer filename has the format::
|
||||
|
||||
name-ver.arch(-pyver).exe
|
||||
|
||||
Some things to note:
|
||||
|
||||
1. The installer filename is not definitive. An installer can be renamed
|
||||
and work perfectly well as an installer. So more reliable data should
|
||||
be used whenever possible.
|
||||
2. The egg-info data should be preferred for the name and version, because
|
||||
these come straight from the distutils metadata, and are mandatory.
|
||||
3. The pyver from the egg-info data should be ignored, as it is
|
||||
constructed from the version of Python used to build the installer,
|
||||
which is irrelevant - the installer filename is correct here (even to
|
||||
the point that when it's not there, any version is implied).
|
||||
4. The architecture must be taken from the installer filename, as it is
|
||||
not included in the egg-info data.
|
||||
5. Architecture-neutral installers still have an architecture because the
|
||||
installer format itself (being executable) is architecture-specific. We
|
||||
should therefore ignore the architecture if the content is pure-python.
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
self.metadata = Message()
|
||||
|
||||
# Determine the initial architecture and Python version from the file name
|
||||
# (if possible)
|
||||
if match := wininst_re.search(path.name):
|
||||
self.platform = normalize(match.group("platform"))
|
||||
if pyver := match.group("pyver"):
|
||||
self.pyver = pyver.replace(".", "")
|
||||
|
||||
# Look for an .egg-info directory and any .pyd files for more precise info
|
||||
egg_info_found = pyd_found = False
|
||||
with ZipFile(self.path) as zip_file:
|
||||
for filename in zip_file.namelist():
|
||||
prefix, filename = filename.split("/", 1)
|
||||
if not egg_info_found and (match := egg_info_re.match(filename)):
|
||||
egg_info_found = True
|
||||
self.name = normalize(match.group("name"))
|
||||
self.version = match.group("ver")
|
||||
if pyver := match.group("pyver"):
|
||||
self.pyver = pyver.replace(".", "")
|
||||
elif not pyd_found and (match := pyd_re.search(filename)):
|
||||
pyd_found = True
|
||||
self.abi = match.group("abi")
|
||||
self.platform = match.group("platform")
|
||||
|
||||
if egg_info_found and pyd_found:
|
||||
break
|
||||
|
||||
def generate_contents(self) -> Iterator[tuple[str, bytes]]:
|
||||
dist_info_dir = f"{self.name}-{self.version}.dist-info"
|
||||
data_dir = f"{self.name}-{self.version}.data"
|
||||
with ZipFile(self.path, "r") as zip_file:
|
||||
for filename in sorted(zip_file.namelist()):
|
||||
# Skip pure directory entries
|
||||
if filename.endswith("/"):
|
||||
continue
|
||||
|
||||
# Handle files in the egg-info directory specially, selectively moving
|
||||
# them to the dist-info directory while converting as needed
|
||||
prefix, target_filename = filename.split("/", 1)
|
||||
if egg_info_re.search(target_filename):
|
||||
basename = target_filename.rsplit("/", 1)[-1]
|
||||
if basename == "requires.txt":
|
||||
requires = zip_file.read(filename).decode("utf-8")
|
||||
convert_requires(requires, self.metadata)
|
||||
elif basename == "PKG-INFO":
|
||||
pkginfo = zip_file.read(filename).decode("utf-8")
|
||||
convert_pkg_info(pkginfo, self.metadata)
|
||||
elif basename == "entry_points.txt":
|
||||
yield (
|
||||
f"{dist_info_dir}/entry_points.txt",
|
||||
zip_file.read(filename),
|
||||
)
|
||||
|
||||
continue
|
||||
elif prefix == "SCRIPTS":
|
||||
target_filename = f"{data_dir}/scripts/{target_filename}"
|
||||
|
||||
# For any other file, just pass it through
|
||||
yield target_filename, zip_file.read(filename)
|
||||
|
||||
|
||||
def convert(files: list[str], dest_dir: str, verbose: bool) -> None:
|
||||
for pat in files:
|
||||
for archive in iglob(pat):
|
||||
path = Path(archive)
|
||||
if path.suffix == ".egg":
|
||||
if path.is_dir():
|
||||
source: ConvertSource = EggDirectorySource(path)
|
||||
else:
|
||||
source = EggFileSource(path)
|
||||
else:
|
||||
source = WininstFileSource(path)
|
||||
|
||||
if verbose:
|
||||
print(f"{archive}...", flush=True, end="")
|
||||
|
||||
dest_path = Path(dest_dir) / (
|
||||
f"{source.name}-{source.version}-{source.pyver}-{source.abi}"
|
||||
f"-{source.platform}.whl"
|
||||
)
|
||||
with WheelFile(dest_path, "w") as wheelfile:
|
||||
for name_or_zinfo, contents in source.generate_contents():
|
||||
wheelfile.writestr(name_or_zinfo, contents)
|
||||
|
||||
# Write the METADATA file
|
||||
wheelfile.writestr(
|
||||
f"{source.dist_info_dir}/METADATA",
|
||||
source.metadata.as_string(policy=serialization_policy).encode(
|
||||
"utf-8"
|
||||
),
|
||||
)
|
||||
|
||||
# Write the WHEEL file
|
||||
wheel_message = Message()
|
||||
wheel_message.add_header("Wheel-Version", "1.0")
|
||||
wheel_message.add_header("Generator", GENERATOR)
|
||||
wheel_message.add_header(
|
||||
"Root-Is-Purelib", str(source.platform == "any").lower()
|
||||
)
|
||||
tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}")
|
||||
for tag in sorted(tags, key=lambda tag: tag.interpreter):
|
||||
wheel_message.add_header("Tag", str(tag))
|
||||
|
||||
wheelfile.writestr(
|
||||
f"{source.dist_info_dir}/WHEEL",
|
||||
wheel_message.as_string(policy=serialization_policy).encode(
|
||||
"utf-8"
|
||||
),
|
||||
)
|
||||
|
||||
if verbose:
|
||||
print("OK")
|
||||
84
.venv/lib/python3.11/site-packages/wheel/_commands/pack.py
Normal file
84
.venv/lib/python3.11/site-packages/wheel/_commands/pack.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import email.policy
|
||||
import os.path
|
||||
import re
|
||||
from email.generator import BytesGenerator
|
||||
from email.parser import BytesParser
|
||||
|
||||
from ..wheelfile import WheelError, WheelFile
|
||||
|
||||
DIST_INFO_RE = re.compile(r"^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))\.dist-info$")
|
||||
|
||||
|
||||
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
|
||||
wheel file name can be determined.
|
||||
|
||||
:param directory: The unpacked wheel directory
|
||||
:param dest_dir: Destination directory (defaults to the current directory)
|
||||
"""
|
||||
# Find the .dist-info directory
|
||||
dist_info_dirs = [
|
||||
fn
|
||||
for fn in os.listdir(directory)
|
||||
if os.path.isdir(os.path.join(directory, fn)) and DIST_INFO_RE.match(fn)
|
||||
]
|
||||
if len(dist_info_dirs) > 1:
|
||||
raise WheelError(f"Multiple .dist-info directories found in {directory}")
|
||||
elif not dist_info_dirs:
|
||||
raise WheelError(f"No .dist-info directories found in {directory}")
|
||||
|
||||
# Determine the target wheel filename
|
||||
dist_info_dir = dist_info_dirs[0]
|
||||
name_version = DIST_INFO_RE.match(dist_info_dir).group("namever")
|
||||
|
||||
# Read the tags and the existing build number from .dist-info/WHEEL
|
||||
wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL")
|
||||
with open(wheel_file_path, "rb") as f:
|
||||
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(
|
||||
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, "wb") as f:
|
||||
BytesGenerator(f, maxheaderlen=0).flatten(info)
|
||||
|
||||
# Reassemble the tags for the wheel file
|
||||
tagline = compute_tagline(tags)
|
||||
|
||||
# Repack the wheel
|
||||
wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl")
|
||||
with WheelFile(wheel_path, "w") as wf:
|
||||
print(f"Repacking wheel as {wheel_path}...", end="", flush=True)
|
||||
wf.write_files(directory)
|
||||
|
||||
print("OK")
|
||||
|
||||
|
||||
def compute_tagline(tags: list[str]) -> str:
|
||||
"""Compute a tagline from a list of tags.
|
||||
|
||||
:param tags: A list of tags
|
||||
:return: A tagline
|
||||
"""
|
||||
impls = sorted({tag.split("-")[0] for tag in tags})
|
||||
abivers = sorted({tag.split("-")[1] for tag in tags})
|
||||
platforms = sorted({tag.split("-")[2] for tag in tags})
|
||||
return "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)])
|
||||
140
.venv/lib/python3.11/site-packages/wheel/_commands/tags.py
Normal file
140
.venv/lib/python3.11/site-packages/wheel/_commands/tags.py
Normal file
@@ -0,0 +1,140 @@
|
||||
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
|
||||
|
||||
|
||||
def _compute_tags(original_tags: Iterable[str], new_tags: str | None) -> set[str]:
|
||||
"""Add or replace tags. Supports dot-separated tags"""
|
||||
if new_tags is None:
|
||||
return set(original_tags)
|
||||
|
||||
if new_tags.startswith("+"):
|
||||
return {*original_tags, *new_tags[1:].split(".")}
|
||||
|
||||
if new_tags.startswith("-"):
|
||||
return set(original_tags) - set(new_tags[1:].split("."))
|
||||
|
||||
return set(new_tags.split("."))
|
||||
|
||||
|
||||
def tags(
|
||||
wheel: str,
|
||||
python_tags: str | None = None,
|
||||
abi_tags: str | None = None,
|
||||
platform_tags: str | None = None,
|
||||
build_tag: str | None = None,
|
||||
remove: bool = False,
|
||||
) -> str:
|
||||
"""Change the tags on a wheel file.
|
||||
|
||||
The tags are left unchanged if they are not specified. To specify "none",
|
||||
use ["none"]. To append to the previous tags, a tag should start with a
|
||||
"+". If a tag starts with "-", it will be removed from existing tags.
|
||||
Processing is done left to right.
|
||||
|
||||
:param wheel: The paths to the wheels
|
||||
:param python_tags: The Python tags to set
|
||||
:param abi_tags: The ABI tags to set
|
||||
:param platform_tags: The platform tags to set
|
||||
:param build_tag: The build tag to set
|
||||
:param remove: Remove the original wheel
|
||||
"""
|
||||
with WheelFile(wheel, "r") as f:
|
||||
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")
|
||||
build = f.parsed_filename.group("build")
|
||||
original_python_tags = f.parsed_filename.group("pyver").split(".")
|
||||
original_abi_tags = f.parsed_filename.group("abi").split(".")
|
||||
original_plat_tags = f.parsed_filename.group("plat").split(".")
|
||||
|
||||
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}
|
||||
platforms = {tag.split("-")[2] for tag in tags}
|
||||
|
||||
if impls != set(original_python_tags):
|
||||
msg = f"Wheel internal tags {impls!r} != filename tags {original_python_tags!r}"
|
||||
raise AssertionError(msg)
|
||||
|
||||
if abivers != set(original_abi_tags):
|
||||
msg = f"Wheel internal tags {abivers!r} != filename tags {original_abi_tags!r}"
|
||||
raise AssertionError(msg)
|
||||
|
||||
if platforms != set(original_plat_tags):
|
||||
msg = (
|
||||
f"Wheel internal tags {platforms!r} != filename tags {original_plat_tags!r}"
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
if existing_build_tag != build:
|
||||
msg = (
|
||||
f"Incorrect filename '{build}' "
|
||||
f"& *.dist-info/WHEEL '{existing_build_tag}' build numbers"
|
||||
)
|
||||
raise AssertionError(msg)
|
||||
|
||||
# Start changing as needed
|
||||
if build_tag is not None:
|
||||
build = build_tag
|
||||
|
||||
final_python_tags = sorted(_compute_tags(original_python_tags, python_tags))
|
||||
final_abi_tags = sorted(_compute_tags(original_abi_tags, abi_tags))
|
||||
final_plat_tags = sorted(_compute_tags(original_plat_tags, platform_tags))
|
||||
|
||||
final_tags = [
|
||||
namever,
|
||||
".".join(final_python_tags),
|
||||
".".join(final_abi_tags),
|
||||
".".join(final_plat_tags),
|
||||
]
|
||||
if build:
|
||||
final_tags.insert(1, build)
|
||||
|
||||
final_wheel_name = "-".join(final_tags) + ".whl"
|
||||
|
||||
if original_wheel_name != final_wheel_name:
|
||||
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
|
||||
)
|
||||
final_wheel_path = os.path.join(os.path.dirname(f.filename), final_wheel_name)
|
||||
|
||||
with (
|
||||
WheelFile(original_wheel_path, "r") as fin,
|
||||
WheelFile(final_wheel_path, "w") as fout,
|
||||
):
|
||||
fout.comment = fin.comment # preserve the comment
|
||||
for item in fin.infolist():
|
||||
if item.is_dir():
|
||||
continue
|
||||
if item.filename == f.dist_info_path + "/RECORD":
|
||||
continue
|
||||
if item.filename == f.dist_info_path + "/WHEEL":
|
||||
fout.writestr(item, info.as_bytes())
|
||||
else:
|
||||
fout.writestr(item, fin.read(item))
|
||||
|
||||
if remove:
|
||||
os.remove(original_wheel_path)
|
||||
|
||||
return final_wheel_name
|
||||
30
.venv/lib/python3.11/site-packages/wheel/_commands/unpack.py
Normal file
30
.venv/lib/python3.11/site-packages/wheel/_commands/unpack.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ..wheelfile import WheelFile
|
||||
|
||||
|
||||
def unpack(path: str, dest: str = ".") -> None:
|
||||
"""Unpack a wheel.
|
||||
|
||||
Wheel content will be unpacked to {dest}/{name}-{ver}, where {name}
|
||||
is the package name and {ver} its version.
|
||||
|
||||
:param path: The path to the wheel.
|
||||
:param dest: Destination directory (default to current directory).
|
||||
"""
|
||||
with WheelFile(path) as wf:
|
||||
namever = wf.parsed_filename.group("namever")
|
||||
destination = Path(dest) / namever
|
||||
print(f"Unpacking to: {destination}...", end="", flush=True)
|
||||
for zinfo in wf.filelist:
|
||||
target_path = Path(wf.extract(zinfo, destination))
|
||||
|
||||
# Set permissions to the same values as they were set in the archive
|
||||
# We have to do this manually due to
|
||||
# https://github.com/python/cpython/issues/59999
|
||||
permissions = zinfo.external_attr >> 16 & 0o777
|
||||
target_path.chmod(permissions)
|
||||
|
||||
print("OK")
|
||||
Reference in New Issue
Block a user