hgfhgfhgfhjh
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,301 +0,0 @@
|
|||||||
import collections
|
|
||||||
import functools
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
from typing import IO, Dict, Iterator, NamedTuple, Optional, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
# Python does not provide platform information at sufficient granularity to
|
|
||||||
# identify the architecture of the running executable in some cases, so we
|
|
||||||
# determine it dynamically by reading the information from the running
|
|
||||||
# process. This only applies on Linux, which uses the ELF format.
|
|
||||||
class _ELFFileHeader:
|
|
||||||
# https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
|
|
||||||
class _InvalidELFFileHeader(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid ELF file header was found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ELF_MAGIC_NUMBER = 0x7F454C46
|
|
||||||
ELFCLASS32 = 1
|
|
||||||
ELFCLASS64 = 2
|
|
||||||
ELFDATA2LSB = 1
|
|
||||||
ELFDATA2MSB = 2
|
|
||||||
EM_386 = 3
|
|
||||||
EM_S390 = 22
|
|
||||||
EM_ARM = 40
|
|
||||||
EM_X86_64 = 62
|
|
||||||
EF_ARM_ABIMASK = 0xFF000000
|
|
||||||
EF_ARM_ABI_VER5 = 0x05000000
|
|
||||||
EF_ARM_ABI_FLOAT_HARD = 0x00000400
|
|
||||||
|
|
||||||
def __init__(self, file: IO[bytes]) -> None:
|
|
||||||
def unpack(fmt: str) -> int:
|
|
||||||
try:
|
|
||||||
data = file.read(struct.calcsize(fmt))
|
|
||||||
result: Tuple[int, ...] = struct.unpack(fmt, data)
|
|
||||||
except struct.error:
|
|
||||||
raise _ELFFileHeader._InvalidELFFileHeader()
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
self.e_ident_magic = unpack(">I")
|
|
||||||
if self.e_ident_magic != self.ELF_MAGIC_NUMBER:
|
|
||||||
raise _ELFFileHeader._InvalidELFFileHeader()
|
|
||||||
self.e_ident_class = unpack("B")
|
|
||||||
if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}:
|
|
||||||
raise _ELFFileHeader._InvalidELFFileHeader()
|
|
||||||
self.e_ident_data = unpack("B")
|
|
||||||
if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}:
|
|
||||||
raise _ELFFileHeader._InvalidELFFileHeader()
|
|
||||||
self.e_ident_version = unpack("B")
|
|
||||||
self.e_ident_osabi = unpack("B")
|
|
||||||
self.e_ident_abiversion = unpack("B")
|
|
||||||
self.e_ident_pad = file.read(7)
|
|
||||||
format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H"
|
|
||||||
format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I"
|
|
||||||
format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q"
|
|
||||||
format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q
|
|
||||||
self.e_type = unpack(format_h)
|
|
||||||
self.e_machine = unpack(format_h)
|
|
||||||
self.e_version = unpack(format_i)
|
|
||||||
self.e_entry = unpack(format_p)
|
|
||||||
self.e_phoff = unpack(format_p)
|
|
||||||
self.e_shoff = unpack(format_p)
|
|
||||||
self.e_flags = unpack(format_i)
|
|
||||||
self.e_ehsize = unpack(format_h)
|
|
||||||
self.e_phentsize = unpack(format_h)
|
|
||||||
self.e_phnum = unpack(format_h)
|
|
||||||
self.e_shentsize = unpack(format_h)
|
|
||||||
self.e_shnum = unpack(format_h)
|
|
||||||
self.e_shstrndx = unpack(format_h)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_elf_header() -> Optional[_ELFFileHeader]:
|
|
||||||
try:
|
|
||||||
with open(sys.executable, "rb") as f:
|
|
||||||
elf_header = _ELFFileHeader(f)
|
|
||||||
except (OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader):
|
|
||||||
return None
|
|
||||||
return elf_header
|
|
||||||
|
|
||||||
|
|
||||||
def _is_linux_armhf() -> bool:
|
|
||||||
# hard-float ABI can be detected from the ELF header of the running
|
|
||||||
# process
|
|
||||||
# https://static.docs.arm.com/ihi0044/g/aaelf32.pdf
|
|
||||||
elf_header = _get_elf_header()
|
|
||||||
if elf_header is None:
|
|
||||||
return False
|
|
||||||
result = elf_header.e_ident_class == elf_header.ELFCLASS32
|
|
||||||
result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
|
|
||||||
result &= elf_header.e_machine == elf_header.EM_ARM
|
|
||||||
result &= (
|
|
||||||
elf_header.e_flags & elf_header.EF_ARM_ABIMASK
|
|
||||||
) == elf_header.EF_ARM_ABI_VER5
|
|
||||||
result &= (
|
|
||||||
elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD
|
|
||||||
) == elf_header.EF_ARM_ABI_FLOAT_HARD
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _is_linux_i686() -> bool:
|
|
||||||
elf_header = _get_elf_header()
|
|
||||||
if elf_header is None:
|
|
||||||
return False
|
|
||||||
result = elf_header.e_ident_class == elf_header.ELFCLASS32
|
|
||||||
result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB
|
|
||||||
result &= elf_header.e_machine == elf_header.EM_386
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _have_compatible_abi(arch: str) -> bool:
|
|
||||||
if arch == "armv7l":
|
|
||||||
return _is_linux_armhf()
|
|
||||||
if arch == "i686":
|
|
||||||
return _is_linux_i686()
|
|
||||||
return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"}
|
|
||||||
|
|
||||||
|
|
||||||
# If glibc ever changes its major version, we need to know what the last
|
|
||||||
# minor version was, so we can build the complete list of all versions.
|
|
||||||
# For now, guess what the highest minor version might be, assume it will
|
|
||||||
# be 50 for testing. Once this actually happens, update the dictionary
|
|
||||||
# with the actual value.
|
|
||||||
_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50)
|
|
||||||
|
|
||||||
|
|
||||||
class _GLibCVersion(NamedTuple):
|
|
||||||
major: int
|
|
||||||
minor: int
|
|
||||||
|
|
||||||
|
|
||||||
def _glibc_version_string_confstr() -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Primary implementation of glibc_version_string using os.confstr.
|
|
||||||
"""
|
|
||||||
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
|
|
||||||
# to be broken or missing. This strategy is used in the standard library
|
|
||||||
# platform module.
|
|
||||||
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
|
|
||||||
try:
|
|
||||||
# os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17".
|
|
||||||
version_string = os.confstr("CS_GNU_LIBC_VERSION")
|
|
||||||
assert version_string is not None
|
|
||||||
_, version = version_string.split()
|
|
||||||
except (AssertionError, AttributeError, OSError, ValueError):
|
|
||||||
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
|
|
||||||
return None
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
|
||||||
def _glibc_version_string_ctypes() -> Optional[str]:
|
|
||||||
"""
|
|
||||||
Fallback implementation of glibc_version_string using ctypes.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import ctypes
|
|
||||||
except ImportError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
|
|
||||||
# manpage says, "If filename is NULL, then the returned handle is for the
|
|
||||||
# main program". This way we can let the linker do the work to figure out
|
|
||||||
# which libc our process is actually using.
|
|
||||||
#
|
|
||||||
# We must also handle the special case where the executable is not a
|
|
||||||
# dynamically linked executable. This can occur when using musl libc,
|
|
||||||
# for example. In this situation, dlopen() will error, leading to an
|
|
||||||
# OSError. Interestingly, at least in the case of musl, there is no
|
|
||||||
# errno set on the OSError. The single string argument used to construct
|
|
||||||
# OSError comes from libc itself and is therefore not portable to
|
|
||||||
# hard code here. In any case, failure to call dlopen() means we
|
|
||||||
# can proceed, so we bail on our attempt.
|
|
||||||
try:
|
|
||||||
process_namespace = ctypes.CDLL(None)
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
gnu_get_libc_version = process_namespace.gnu_get_libc_version
|
|
||||||
except AttributeError:
|
|
||||||
# Symbol doesn't exist -> therefore, we are not linked to
|
|
||||||
# glibc.
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Call gnu_get_libc_version, which returns a string like "2.5"
|
|
||||||
gnu_get_libc_version.restype = ctypes.c_char_p
|
|
||||||
version_str: str = gnu_get_libc_version()
|
|
||||||
# py2 / py3 compatibility:
|
|
||||||
if not isinstance(version_str, str):
|
|
||||||
version_str = version_str.decode("ascii")
|
|
||||||
|
|
||||||
return version_str
|
|
||||||
|
|
||||||
|
|
||||||
def _glibc_version_string() -> Optional[str]:
|
|
||||||
"""Returns glibc version string, or None if not using glibc."""
|
|
||||||
return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
|
|
||||||
"""Parse glibc version.
|
|
||||||
|
|
||||||
We use a regexp instead of str.split because we want to discard any
|
|
||||||
random junk that might come after the minor version -- this might happen
|
|
||||||
in patched/forked versions of glibc (e.g. Linaro's version of glibc
|
|
||||||
uses version strings like "2.20-2014.11"). See gh-3588.
|
|
||||||
"""
|
|
||||||
m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)
|
|
||||||
if not m:
|
|
||||||
warnings.warn(
|
|
||||||
"Expected glibc version with 2 components major.minor,"
|
|
||||||
" got: %s" % version_str,
|
|
||||||
RuntimeWarning,
|
|
||||||
)
|
|
||||||
return -1, -1
|
|
||||||
return int(m.group("major")), int(m.group("minor"))
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def _get_glibc_version() -> Tuple[int, int]:
|
|
||||||
version_str = _glibc_version_string()
|
|
||||||
if version_str is None:
|
|
||||||
return (-1, -1)
|
|
||||||
return _parse_glibc_version(version_str)
|
|
||||||
|
|
||||||
|
|
||||||
# From PEP 513, PEP 600
|
|
||||||
def _is_compatible(name: str, 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
|
|
||||||
except ImportError:
|
|
||||||
return True
|
|
||||||
if hasattr(_manylinux, "manylinux_compatible"):
|
|
||||||
result = _manylinux.manylinux_compatible(version[0], version[1], arch)
|
|
||||||
if result is not None:
|
|
||||||
return bool(result)
|
|
||||||
return True
|
|
||||||
if version == _GLibCVersion(2, 5):
|
|
||||||
if hasattr(_manylinux, "manylinux1_compatible"):
|
|
||||||
return bool(_manylinux.manylinux1_compatible)
|
|
||||||
if version == _GLibCVersion(2, 12):
|
|
||||||
if hasattr(_manylinux, "manylinux2010_compatible"):
|
|
||||||
return bool(_manylinux.manylinux2010_compatible)
|
|
||||||
if version == _GLibCVersion(2, 17):
|
|
||||||
if hasattr(_manylinux, "manylinux2014_compatible"):
|
|
||||||
return bool(_manylinux.manylinux2014_compatible)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
_LEGACY_MANYLINUX_MAP = {
|
|
||||||
# CentOS 7 w/ glibc 2.17 (PEP 599)
|
|
||||||
(2, 17): "manylinux2014",
|
|
||||||
# CentOS 6 w/ glibc 2.12 (PEP 571)
|
|
||||||
(2, 12): "manylinux2010",
|
|
||||||
# CentOS 5 w/ glibc 2.5 (PEP 513)
|
|
||||||
(2, 5): "manylinux1",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def platform_tags(linux: str, arch: str) -> Iterator[str]:
|
|
||||||
if not _have_compatible_abi(arch):
|
|
||||||
return
|
|
||||||
# Oldest glibc to be supported regardless of architecture is (2, 17).
|
|
||||||
too_old_glibc2 = _GLibCVersion(2, 16)
|
|
||||||
if arch in {"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())
|
|
||||||
glibc_max_list = [current_glibc]
|
|
||||||
# We can assume compatibility across glibc major versions.
|
|
||||||
# https://sourceware.org/bugzilla/show_bug.cgi?id=24636
|
|
||||||
#
|
|
||||||
# Build a list of maximum glibc versions so that we can
|
|
||||||
# output the canonical list of all glibc from current_glibc
|
|
||||||
# down to too_old_glibc2, including all intermediary versions.
|
|
||||||
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)
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"""PEP 656 support.
|
|
||||||
|
|
||||||
This module implements logic to detect if the currently running Python is
|
|
||||||
linked against musl, and what musl version is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import functools
|
|
||||||
import operator
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from typing import IO, Iterator, NamedTuple, Optional, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
def _read_unpacked(f: IO[bytes], fmt: str) -> Tuple[int, ...]:
|
|
||||||
return struct.unpack(fmt, f.read(struct.calcsize(fmt)))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_ld_musl_from_elf(f: IO[bytes]) -> Optional[str]:
|
|
||||||
"""Detect musl libc location by parsing the Python executable.
|
|
||||||
|
|
||||||
Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
|
|
||||||
ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
|
||||||
"""
|
|
||||||
f.seek(0)
|
|
||||||
try:
|
|
||||||
ident = _read_unpacked(f, "16B")
|
|
||||||
except struct.error:
|
|
||||||
return None
|
|
||||||
if ident[:4] != tuple(b"\x7fELF"): # Invalid magic, not ELF.
|
|
||||||
return None
|
|
||||||
f.seek(struct.calcsize("HHI"), 1) # Skip file type, machine, and version.
|
|
||||||
|
|
||||||
try:
|
|
||||||
# e_fmt: Format for program header.
|
|
||||||
# p_fmt: Format for section header.
|
|
||||||
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
|
|
||||||
e_fmt, p_fmt, p_idx = {
|
|
||||||
1: ("IIIIHHH", "IIIIIIII", (0, 1, 4)), # 32-bit.
|
|
||||||
2: ("QQQIHHH", "IIQQQQQQ", (0, 2, 5)), # 64-bit.
|
|
||||||
}[ident[4]]
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
p_get = operator.itemgetter(*p_idx)
|
|
||||||
|
|
||||||
# Find the interpreter section and return its content.
|
|
||||||
try:
|
|
||||||
_, e_phoff, _, _, _, e_phentsize, e_phnum = _read_unpacked(f, e_fmt)
|
|
||||||
except struct.error:
|
|
||||||
return None
|
|
||||||
for i in range(e_phnum + 1):
|
|
||||||
f.seek(e_phoff + e_phentsize * i)
|
|
||||||
try:
|
|
||||||
p_type, p_offset, p_filesz = p_get(_read_unpacked(f, p_fmt))
|
|
||||||
except struct.error:
|
|
||||||
return None
|
|
||||||
if p_type != 3: # Not PT_INTERP.
|
|
||||||
continue
|
|
||||||
f.seek(p_offset)
|
|
||||||
interpreter = os.fsdecode(f.read(p_filesz)).strip("\0")
|
|
||||||
if "musl" not in interpreter:
|
|
||||||
return None
|
|
||||||
return interpreter
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class _MuslVersion(NamedTuple):
|
|
||||||
major: int
|
|
||||||
minor: int
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
|
|
||||||
lines = [n for n in (n.strip() for n in output.splitlines()) if n]
|
|
||||||
if len(lines) < 2 or lines[0][:4] != "musl":
|
|
||||||
return None
|
|
||||||
m = re.match(r"Version (\d+)\.(\d+)", lines[1])
|
|
||||||
if not m:
|
|
||||||
return None
|
|
||||||
return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
|
|
||||||
"""Detect currently-running musl runtime version.
|
|
||||||
|
|
||||||
This is done by checking the specified executable's dynamic linking
|
|
||||||
information, and invoking the loader to parse its output for a version
|
|
||||||
string. If the loader is musl, the output would be something like::
|
|
||||||
|
|
||||||
musl libc (x86_64)
|
|
||||||
Version 1.2.2
|
|
||||||
Dynamic Program Loader
|
|
||||||
"""
|
|
||||||
with contextlib.ExitStack() as stack:
|
|
||||||
try:
|
|
||||||
f = stack.enter_context(open(executable, "rb"))
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
ld = _parse_ld_musl_from_elf(f)
|
|
||||||
if not ld:
|
|
||||||
return None
|
|
||||||
proc = subprocess.run([ld], stderr=subprocess.PIPE, universal_newlines=True)
|
|
||||||
return _parse_musl_version(proc.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def platform_tags(arch: 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.
|
|
||||||
|
|
||||||
: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}"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
|
||||||
import sysconfig
|
|
||||||
|
|
||||||
plat = sysconfig.get_platform()
|
|
||||||
assert plat.startswith("linux-"), "not linux"
|
|
||||||
|
|
||||||
print("plat:", plat)
|
|
||||||
print("musl:", _get_musl_version(sys.executable))
|
|
||||||
print("tags:", end=" ")
|
|
||||||
for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])):
|
|
||||||
print(t, end="\n ")
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
class InfinityType:
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return "Infinity"
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(repr(self))
|
|
||||||
|
|
||||||
def __lt__(self, other: object) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __le__(self, other: object) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
return isinstance(other, self.__class__)
|
|
||||||
|
|
||||||
def __gt__(self, other: object) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __ge__(self, other: object) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __neg__(self: object) -> "NegativeInfinityType":
|
|
||||||
return NegativeInfinity
|
|
||||||
|
|
||||||
|
|
||||||
Infinity = InfinityType()
|
|
||||||
|
|
||||||
|
|
||||||
class NegativeInfinityType:
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return "-Infinity"
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(repr(self))
|
|
||||||
|
|
||||||
def __lt__(self, other: object) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __le__(self, other: object) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
return isinstance(other, self.__class__)
|
|
||||||
|
|
||||||
def __gt__(self, other: object) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __ge__(self, other: object) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __neg__(self: object) -> InfinityType:
|
|
||||||
return Infinity
|
|
||||||
|
|
||||||
|
|
||||||
NegativeInfinity = NegativeInfinityType()
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import operator
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import sys
|
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
||||||
|
|
||||||
from setuptools.extern.pyparsing import ( # noqa: N817
|
|
||||||
Forward,
|
|
||||||
Group,
|
|
||||||
Literal as L,
|
|
||||||
ParseException,
|
|
||||||
ParseResults,
|
|
||||||
QuotedString,
|
|
||||||
ZeroOrMore,
|
|
||||||
stringEnd,
|
|
||||||
stringStart,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .specifiers import InvalidSpecifier, Specifier
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"InvalidMarker",
|
|
||||||
"UndefinedComparison",
|
|
||||||
"UndefinedEnvironmentName",
|
|
||||||
"Marker",
|
|
||||||
"default_environment",
|
|
||||||
]
|
|
||||||
|
|
||||||
Operator = Callable[[str, str], bool]
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidMarker(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid marker was found, users should refer to PEP 508.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class UndefinedComparison(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid operation was attempted on a value that doesn't support it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class UndefinedEnvironmentName(ValueError):
|
|
||||||
"""
|
|
||||||
A name was attempted to be used that does not exist inside of the
|
|
||||||
environment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Node:
|
|
||||||
def __init__(self, value: Any) -> None:
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return str(self.value)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<{self.__class__.__name__}('{self}')>"
|
|
||||||
|
|
||||||
def serialize(self) -> str:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class Variable(Node):
|
|
||||||
def serialize(self) -> str:
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Value(Node):
|
|
||||||
def serialize(self) -> str:
|
|
||||||
return f'"{self}"'
|
|
||||||
|
|
||||||
|
|
||||||
class Op(Node):
|
|
||||||
def serialize(self) -> str:
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
|
|
||||||
VARIABLE = (
|
|
||||||
L("implementation_version")
|
|
||||||
| L("platform_python_implementation")
|
|
||||||
| L("implementation_name")
|
|
||||||
| L("python_full_version")
|
|
||||||
| L("platform_release")
|
|
||||||
| L("platform_version")
|
|
||||||
| L("platform_machine")
|
|
||||||
| L("platform_system")
|
|
||||||
| L("python_version")
|
|
||||||
| L("sys_platform")
|
|
||||||
| L("os_name")
|
|
||||||
| L("os.name") # PEP-345
|
|
||||||
| L("sys.platform") # PEP-345
|
|
||||||
| L("platform.version") # PEP-345
|
|
||||||
| L("platform.machine") # PEP-345
|
|
||||||
| L("platform.python_implementation") # PEP-345
|
|
||||||
| L("python_implementation") # undocumented setuptools legacy
|
|
||||||
| L("extra") # PEP-508
|
|
||||||
)
|
|
||||||
ALIASES = {
|
|
||||||
"os.name": "os_name",
|
|
||||||
"sys.platform": "sys_platform",
|
|
||||||
"platform.version": "platform_version",
|
|
||||||
"platform.machine": "platform_machine",
|
|
||||||
"platform.python_implementation": "platform_python_implementation",
|
|
||||||
"python_implementation": "platform_python_implementation",
|
|
||||||
}
|
|
||||||
VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))
|
|
||||||
|
|
||||||
VERSION_CMP = (
|
|
||||||
L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<")
|
|
||||||
)
|
|
||||||
|
|
||||||
MARKER_OP = VERSION_CMP | L("not in") | L("in")
|
|
||||||
MARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))
|
|
||||||
|
|
||||||
MARKER_VALUE = QuotedString("'") | QuotedString('"')
|
|
||||||
MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))
|
|
||||||
|
|
||||||
BOOLOP = L("and") | L("or")
|
|
||||||
|
|
||||||
MARKER_VAR = VARIABLE | MARKER_VALUE
|
|
||||||
|
|
||||||
MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)
|
|
||||||
MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))
|
|
||||||
|
|
||||||
LPAREN = L("(").suppress()
|
|
||||||
RPAREN = L(")").suppress()
|
|
||||||
|
|
||||||
MARKER_EXPR = Forward()
|
|
||||||
MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)
|
|
||||||
MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)
|
|
||||||
|
|
||||||
MARKER = stringStart + MARKER_EXPR + stringEnd
|
|
||||||
|
|
||||||
|
|
||||||
def _coerce_parse_result(results: Union[ParseResults, List[Any]]) -> List[Any]:
|
|
||||||
if isinstance(results, ParseResults):
|
|
||||||
return [_coerce_parse_result(i) for i in results]
|
|
||||||
else:
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def _format_marker(
|
|
||||||
marker: Union[List[str], Tuple[Node, ...], str], first: Optional[bool] = True
|
|
||||||
) -> str:
|
|
||||||
|
|
||||||
assert isinstance(marker, (list, tuple, str))
|
|
||||||
|
|
||||||
# Sometimes we have a structure like [[...]] which is a single item list
|
|
||||||
# where the single item is itself it's own list. In that case we want skip
|
|
||||||
# the rest of this function so that we don't get extraneous () on the
|
|
||||||
# outside.
|
|
||||||
if (
|
|
||||||
isinstance(marker, list)
|
|
||||||
and len(marker) == 1
|
|
||||||
and isinstance(marker[0], (list, tuple))
|
|
||||||
):
|
|
||||||
return _format_marker(marker[0])
|
|
||||||
|
|
||||||
if isinstance(marker, list):
|
|
||||||
inner = (_format_marker(m, first=False) for m in marker)
|
|
||||||
if first:
|
|
||||||
return " ".join(inner)
|
|
||||||
else:
|
|
||||||
return "(" + " ".join(inner) + ")"
|
|
||||||
elif isinstance(marker, tuple):
|
|
||||||
return " ".join([m.serialize() for m in marker])
|
|
||||||
else:
|
|
||||||
return marker
|
|
||||||
|
|
||||||
|
|
||||||
_operators: Dict[str, Operator] = {
|
|
||||||
"in": lambda lhs, rhs: lhs in rhs,
|
|
||||||
"not in": lambda lhs, rhs: lhs not in rhs,
|
|
||||||
"<": operator.lt,
|
|
||||||
"<=": operator.le,
|
|
||||||
"==": operator.eq,
|
|
||||||
"!=": operator.ne,
|
|
||||||
">=": operator.ge,
|
|
||||||
">": operator.gt,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
|
|
||||||
try:
|
|
||||||
spec = Specifier("".join([op.serialize(), rhs]))
|
|
||||||
except InvalidSpecifier:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return spec.contains(lhs)
|
|
||||||
|
|
||||||
oper: Optional[Operator] = _operators.get(op.serialize())
|
|
||||||
if oper is None:
|
|
||||||
raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
|
|
||||||
|
|
||||||
return oper(lhs, rhs)
|
|
||||||
|
|
||||||
|
|
||||||
class Undefined:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_undefined = Undefined()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_env(environment: Dict[str, str], name: str) -> str:
|
|
||||||
value: Union[str, Undefined] = environment.get(name, _undefined)
|
|
||||||
|
|
||||||
if isinstance(value, Undefined):
|
|
||||||
raise UndefinedEnvironmentName(
|
|
||||||
f"{name!r} does not exist in evaluation environment."
|
|
||||||
)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _evaluate_markers(markers: List[Any], environment: Dict[str, str]) -> bool:
|
|
||||||
groups: List[List[bool]] = [[]]
|
|
||||||
|
|
||||||
for marker in markers:
|
|
||||||
assert isinstance(marker, (list, tuple, str))
|
|
||||||
|
|
||||||
if isinstance(marker, list):
|
|
||||||
groups[-1].append(_evaluate_markers(marker, environment))
|
|
||||||
elif isinstance(marker, tuple):
|
|
||||||
lhs, op, rhs = marker
|
|
||||||
|
|
||||||
if isinstance(lhs, Variable):
|
|
||||||
lhs_value = _get_env(environment, lhs.value)
|
|
||||||
rhs_value = rhs.value
|
|
||||||
else:
|
|
||||||
lhs_value = lhs.value
|
|
||||||
rhs_value = _get_env(environment, rhs.value)
|
|
||||||
|
|
||||||
groups[-1].append(_eval_op(lhs_value, op, rhs_value))
|
|
||||||
else:
|
|
||||||
assert marker in ["and", "or"]
|
|
||||||
if marker == "or":
|
|
||||||
groups.append([])
|
|
||||||
|
|
||||||
return any(all(item) for item in groups)
|
|
||||||
|
|
||||||
|
|
||||||
def format_full_version(info: "sys._version_info") -> str:
|
|
||||||
version = "{0.major}.{0.minor}.{0.micro}".format(info)
|
|
||||||
kind = info.releaselevel
|
|
||||||
if kind != "final":
|
|
||||||
version += kind[0] + str(info.serial)
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
|
||||||
def default_environment() -> Dict[str, str]:
|
|
||||||
iver = format_full_version(sys.implementation.version)
|
|
||||||
implementation_name = sys.implementation.name
|
|
||||||
return {
|
|
||||||
"implementation_name": implementation_name,
|
|
||||||
"implementation_version": iver,
|
|
||||||
"os_name": os.name,
|
|
||||||
"platform_machine": platform.machine(),
|
|
||||||
"platform_release": platform.release(),
|
|
||||||
"platform_system": platform.system(),
|
|
||||||
"platform_version": platform.version(),
|
|
||||||
"python_full_version": platform.python_version(),
|
|
||||||
"platform_python_implementation": platform.python_implementation(),
|
|
||||||
"python_version": ".".join(platform.python_version_tuple()[:2]),
|
|
||||||
"sys_platform": sys.platform,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Marker:
|
|
||||||
def __init__(self, marker: str) -> None:
|
|
||||||
try:
|
|
||||||
self._markers = _coerce_parse_result(MARKER.parseString(marker))
|
|
||||||
except ParseException as e:
|
|
||||||
raise InvalidMarker(
|
|
||||||
f"Invalid marker: {marker!r}, parse error at "
|
|
||||||
f"{marker[e.loc : e.loc + 8]!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return _format_marker(self._markers)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<Marker('{self}')>"
|
|
||||||
|
|
||||||
def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
|
|
||||||
"""Evaluate a marker.
|
|
||||||
|
|
||||||
Return the boolean from evaluating the given marker against the
|
|
||||||
environment. environment is an optional argument to override all or
|
|
||||||
part of the determined environment.
|
|
||||||
|
|
||||||
The environment is determined from the current Python process.
|
|
||||||
"""
|
|
||||||
current_environment = default_environment()
|
|
||||||
if environment is not None:
|
|
||||||
current_environment.update(environment)
|
|
||||||
|
|
||||||
return _evaluate_markers(self._markers, current_environment)
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
import urllib.parse
|
|
||||||
from typing import List, Optional as TOptional, Set
|
|
||||||
|
|
||||||
from setuptools.extern.pyparsing import ( # noqa
|
|
||||||
Combine,
|
|
||||||
Literal as L,
|
|
||||||
Optional,
|
|
||||||
ParseException,
|
|
||||||
Regex,
|
|
||||||
Word,
|
|
||||||
ZeroOrMore,
|
|
||||||
originalTextFor,
|
|
||||||
stringEnd,
|
|
||||||
stringStart,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .markers import MARKER_EXPR, Marker
|
|
||||||
from .specifiers import LegacySpecifier, Specifier, SpecifierSet
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidRequirement(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid requirement was found, users should refer to PEP 508.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
ALPHANUM = Word(string.ascii_letters + string.digits)
|
|
||||||
|
|
||||||
LBRACKET = L("[").suppress()
|
|
||||||
RBRACKET = L("]").suppress()
|
|
||||||
LPAREN = L("(").suppress()
|
|
||||||
RPAREN = L(")").suppress()
|
|
||||||
COMMA = L(",").suppress()
|
|
||||||
SEMICOLON = L(";").suppress()
|
|
||||||
AT = L("@").suppress()
|
|
||||||
|
|
||||||
PUNCTUATION = Word("-_.")
|
|
||||||
IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)
|
|
||||||
IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
|
|
||||||
|
|
||||||
NAME = IDENTIFIER("name")
|
|
||||||
EXTRA = IDENTIFIER
|
|
||||||
|
|
||||||
URI = Regex(r"[^ ]+")("url")
|
|
||||||
URL = AT + URI
|
|
||||||
|
|
||||||
EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
|
|
||||||
EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
|
|
||||||
|
|
||||||
VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)
|
|
||||||
VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)
|
|
||||||
|
|
||||||
VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY
|
|
||||||
VERSION_MANY = Combine(
|
|
||||||
VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False
|
|
||||||
)("_raw_spec")
|
|
||||||
_VERSION_SPEC = Optional((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)
|
|
||||||
_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "")
|
|
||||||
|
|
||||||
VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier")
|
|
||||||
VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
|
|
||||||
|
|
||||||
MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
|
|
||||||
MARKER_EXPR.setParseAction(
|
|
||||||
lambda s, l, t: Marker(s[t._original_start : t._original_end])
|
|
||||||
)
|
|
||||||
MARKER_SEPARATOR = SEMICOLON
|
|
||||||
MARKER = MARKER_SEPARATOR + MARKER_EXPR
|
|
||||||
|
|
||||||
VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)
|
|
||||||
URL_AND_MARKER = URL + Optional(MARKER)
|
|
||||||
|
|
||||||
NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
|
|
||||||
|
|
||||||
REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
|
|
||||||
# setuptools.extern.pyparsing isn't thread safe during initialization, so we do it eagerly, see
|
|
||||||
# issue #104
|
|
||||||
REQUIREMENT.parseString("x[]")
|
|
||||||
|
|
||||||
|
|
||||||
class Requirement:
|
|
||||||
"""Parse a requirement.
|
|
||||||
|
|
||||||
Parse a given requirement string into its parts, such as name, specifier,
|
|
||||||
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
|
|
||||||
string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO: Can we test whether something is contained within a requirement?
|
|
||||||
# If so how do we do that? Do we need to test against the _name_ of
|
|
||||||
# the thing as well as the version? What about the markers?
|
|
||||||
# TODO: Can we normalize the name and extra name?
|
|
||||||
|
|
||||||
def __init__(self, requirement_string: str) -> None:
|
|
||||||
try:
|
|
||||||
req = REQUIREMENT.parseString(requirement_string)
|
|
||||||
except ParseException as e:
|
|
||||||
raise InvalidRequirement(
|
|
||||||
f'Parse error at "{ requirement_string[e.loc : e.loc + 8]!r}": {e.msg}'
|
|
||||||
)
|
|
||||||
|
|
||||||
self.name: str = req.name
|
|
||||||
if req.url:
|
|
||||||
parsed_url = urllib.parse.urlparse(req.url)
|
|
||||||
if parsed_url.scheme == "file":
|
|
||||||
if urllib.parse.urlunparse(parsed_url) != req.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: {req.url}")
|
|
||||||
self.url: TOptional[str] = req.url
|
|
||||||
else:
|
|
||||||
self.url = None
|
|
||||||
self.extras: Set[str] = set(req.extras.asList() if req.extras else [])
|
|
||||||
self.specifier: SpecifierSet = SpecifierSet(req.specifier)
|
|
||||||
self.marker: TOptional[Marker] = req.marker if req.marker else None
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
parts: List[str] = [self.name]
|
|
||||||
|
|
||||||
if self.extras:
|
|
||||||
formatted_extras = ",".join(sorted(self.extras))
|
|
||||||
parts.append(f"[{formatted_extras}]")
|
|
||||||
|
|
||||||
if self.specifier:
|
|
||||||
parts.append(str(self.specifier))
|
|
||||||
|
|
||||||
if self.url:
|
|
||||||
parts.append(f"@ {self.url}")
|
|
||||||
if self.marker:
|
|
||||||
parts.append(" ")
|
|
||||||
|
|
||||||
if self.marker:
|
|
||||||
parts.append(f"; {self.marker}")
|
|
||||||
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<Requirement('{self}')>"
|
|
||||||
@@ -1,802 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import functools
|
|
||||||
import itertools
|
|
||||||
import re
|
|
||||||
import warnings
|
|
||||||
from typing import (
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Pattern,
|
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .utils import canonicalize_version
|
|
||||||
from .version import LegacyVersion, Version, parse
|
|
||||||
|
|
||||||
ParsedVersion = Union[Version, LegacyVersion]
|
|
||||||
UnparsedVersion = Union[Version, LegacyVersion, str]
|
|
||||||
VersionTypeVar = TypeVar("VersionTypeVar", bound=UnparsedVersion)
|
|
||||||
CallableOperator = Callable[[ParsedVersion, str], bool]
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSpecifier(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid specifier was found, users should refer to PEP 440.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BaseSpecifier(metaclass=abc.ABCMeta):
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""
|
|
||||||
Returns the str representation of this Specifier like object. This
|
|
||||||
should be representative of the Specifier itself.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
"""
|
|
||||||
Returns a hash value for this Specifier like object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
"""
|
|
||||||
Returns a boolean representing whether or not the two Specifier like
|
|
||||||
objects are equal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractproperty
|
|
||||||
def prereleases(self) -> Optional[bool]:
|
|
||||||
"""
|
|
||||||
Returns whether or not pre-releases as a whole are allowed by this
|
|
||||||
specifier.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@prereleases.setter
|
|
||||||
def prereleases(self, value: bool) -> None:
|
|
||||||
"""
|
|
||||||
Sets whether or not pre-releases as a whole are allowed by this
|
|
||||||
specifier.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
|
|
||||||
"""
|
|
||||||
Determines if the given item is contained within this specifier.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def filter(
|
|
||||||
self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
|
|
||||||
) -> Iterable[VersionTypeVar]:
|
|
||||||
"""
|
|
||||||
Takes an iterable of items and filters them so that only items which
|
|
||||||
are contained within this specifier are allowed in it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _IndividualSpecifier(BaseSpecifier):
|
|
||||||
|
|
||||||
_operators: Dict[str, str] = {}
|
|
||||||
_regex: Pattern[str]
|
|
||||||
|
|
||||||
def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
|
|
||||||
match = self._regex.search(spec)
|
|
||||||
if not match:
|
|
||||||
raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
|
|
||||||
|
|
||||||
self._spec: Tuple[str, str] = (
|
|
||||||
match.group("operator").strip(),
|
|
||||||
match.group("version").strip(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store whether or not this Specifier should accept prereleases
|
|
||||||
self._prereleases = prereleases
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
pre = (
|
|
||||||
f", prereleases={self.prereleases!r}"
|
|
||||||
if self._prereleases is not None
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "{}{}".format(*self._spec)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _canonical_spec(self) -> Tuple[str, str]:
|
|
||||||
return self._spec[0], canonicalize_version(self._spec[1])
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self._canonical_spec)
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if isinstance(other, str):
|
|
||||||
try:
|
|
||||||
other = self.__class__(str(other))
|
|
||||||
except InvalidSpecifier:
|
|
||||||
return NotImplemented
|
|
||||||
elif not isinstance(other, self.__class__):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._canonical_spec == other._canonical_spec
|
|
||||||
|
|
||||||
def _get_operator(self, op: str) -> CallableOperator:
|
|
||||||
operator_callable: CallableOperator = getattr(
|
|
||||||
self, f"_compare_{self._operators[op]}"
|
|
||||||
)
|
|
||||||
return operator_callable
|
|
||||||
|
|
||||||
def _coerce_version(self, version: UnparsedVersion) -> ParsedVersion:
|
|
||||||
if not isinstance(version, (LegacyVersion, Version)):
|
|
||||||
version = parse(version)
|
|
||||||
return version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def operator(self) -> str:
|
|
||||||
return self._spec[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def version(self) -> str:
|
|
||||||
return self._spec[1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def prereleases(self) -> Optional[bool]:
|
|
||||||
return self._prereleases
|
|
||||||
|
|
||||||
@prereleases.setter
|
|
||||||
def prereleases(self, value: bool) -> None:
|
|
||||||
self._prereleases = value
|
|
||||||
|
|
||||||
def __contains__(self, item: str) -> bool:
|
|
||||||
return self.contains(item)
|
|
||||||
|
|
||||||
def contains(
|
|
||||||
self, item: UnparsedVersion, prereleases: Optional[bool] = None
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
# Determine if prereleases are to be allowed or not.
|
|
||||||
if prereleases is None:
|
|
||||||
prereleases = self.prereleases
|
|
||||||
|
|
||||||
# Normalize item to a Version or LegacyVersion, this allows us to have
|
|
||||||
# a shortcut for ``"2.0" in Specifier(">=2")
|
|
||||||
normalized_item = self._coerce_version(item)
|
|
||||||
|
|
||||||
# Determine if we should be supporting prereleases in this specifier
|
|
||||||
# or not, if we do not support prereleases than we can short circuit
|
|
||||||
# logic if this version is a prereleases.
|
|
||||||
if normalized_item.is_prerelease and not prereleases:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Actually do the comparison to determine if this item is contained
|
|
||||||
# within this Specifier or not.
|
|
||||||
operator_callable: CallableOperator = self._get_operator(self.operator)
|
|
||||||
return operator_callable(normalized_item, self.version)
|
|
||||||
|
|
||||||
def filter(
|
|
||||||
self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
|
|
||||||
) -> Iterable[VersionTypeVar]:
|
|
||||||
|
|
||||||
yielded = False
|
|
||||||
found_prereleases = []
|
|
||||||
|
|
||||||
kw = {"prereleases": prereleases if prereleases is not None else True}
|
|
||||||
|
|
||||||
# Attempt to iterate over all the values in the iterable and if any of
|
|
||||||
# them match, yield them.
|
|
||||||
for version in iterable:
|
|
||||||
parsed_version = self._coerce_version(version)
|
|
||||||
|
|
||||||
if self.contains(parsed_version, **kw):
|
|
||||||
# If our version is a prerelease, and we were not set to allow
|
|
||||||
# prereleases, then we'll store it for later in case nothing
|
|
||||||
# else matches this specifier.
|
|
||||||
if parsed_version.is_prerelease and not (
|
|
||||||
prereleases or self.prereleases
|
|
||||||
):
|
|
||||||
found_prereleases.append(version)
|
|
||||||
# Either this is not a prerelease, or we should have been
|
|
||||||
# accepting prereleases from the beginning.
|
|
||||||
else:
|
|
||||||
yielded = True
|
|
||||||
yield version
|
|
||||||
|
|
||||||
# Now that we've iterated over everything, determine if we've yielded
|
|
||||||
# any values, and if we have not and we have any prereleases stored up
|
|
||||||
# then we will go ahead and yield the prereleases.
|
|
||||||
if not yielded and found_prereleases:
|
|
||||||
for version in found_prereleases:
|
|
||||||
yield version
|
|
||||||
|
|
||||||
|
|
||||||
class LegacySpecifier(_IndividualSpecifier):
|
|
||||||
|
|
||||||
_regex_str = r"""
|
|
||||||
(?P<operator>(==|!=|<=|>=|<|>))
|
|
||||||
\s*
|
|
||||||
(?P<version>
|
|
||||||
[^,;\s)]* # Since this is a "legacy" specifier, and the version
|
|
||||||
# string can be just about anything, we match everything
|
|
||||||
# except for whitespace, a semi-colon for marker support,
|
|
||||||
# a closing paren since versions can be enclosed in
|
|
||||||
# them, and a comma since it's a version separator.
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
_regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
|
|
||||||
|
|
||||||
_operators = {
|
|
||||||
"==": "equal",
|
|
||||||
"!=": "not_equal",
|
|
||||||
"<=": "less_than_equal",
|
|
||||||
">=": "greater_than_equal",
|
|
||||||
"<": "less_than",
|
|
||||||
">": "greater_than",
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
|
|
||||||
super().__init__(spec, prereleases)
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Creating a LegacyVersion has been deprecated and will be "
|
|
||||||
"removed in the next major release",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _coerce_version(self, version: UnparsedVersion) -> LegacyVersion:
|
|
||||||
if not isinstance(version, LegacyVersion):
|
|
||||||
version = LegacyVersion(str(version))
|
|
||||||
return version
|
|
||||||
|
|
||||||
def _compare_equal(self, prospective: LegacyVersion, spec: str) -> bool:
|
|
||||||
return prospective == self._coerce_version(spec)
|
|
||||||
|
|
||||||
def _compare_not_equal(self, prospective: LegacyVersion, spec: str) -> bool:
|
|
||||||
return prospective != self._coerce_version(spec)
|
|
||||||
|
|
||||||
def _compare_less_than_equal(self, prospective: LegacyVersion, spec: str) -> bool:
|
|
||||||
return prospective <= self._coerce_version(spec)
|
|
||||||
|
|
||||||
def _compare_greater_than_equal(
|
|
||||||
self, prospective: LegacyVersion, spec: str
|
|
||||||
) -> bool:
|
|
||||||
return prospective >= self._coerce_version(spec)
|
|
||||||
|
|
||||||
def _compare_less_than(self, prospective: LegacyVersion, spec: str) -> bool:
|
|
||||||
return prospective < self._coerce_version(spec)
|
|
||||||
|
|
||||||
def _compare_greater_than(self, prospective: LegacyVersion, spec: str) -> bool:
|
|
||||||
return prospective > self._coerce_version(spec)
|
|
||||||
|
|
||||||
|
|
||||||
def _require_version_compare(
|
|
||||||
fn: Callable[["Specifier", ParsedVersion, str], bool]
|
|
||||||
) -> Callable[["Specifier", ParsedVersion, str], bool]:
|
|
||||||
@functools.wraps(fn)
|
|
||||||
def wrapped(self: "Specifier", prospective: ParsedVersion, spec: str) -> bool:
|
|
||||||
if not isinstance(prospective, Version):
|
|
||||||
return False
|
|
||||||
return fn(self, prospective, spec)
|
|
||||||
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
class Specifier(_IndividualSpecifier):
|
|
||||||
|
|
||||||
_regex_str = r"""
|
|
||||||
(?P<operator>(~=|==|!=|<=|>=|<|>|===))
|
|
||||||
(?P<version>
|
|
||||||
(?:
|
|
||||||
# The identity operators allow for an escape hatch that will
|
|
||||||
# do an exact string match of the version you wish to install.
|
|
||||||
# This will not be parsed by PEP 440 and we cannot determine
|
|
||||||
# any semantic meaning from it. This operator is discouraged
|
|
||||||
# but included entirely as an escape hatch.
|
|
||||||
(?<====) # Only match for the identity operator
|
|
||||||
\s*
|
|
||||||
[^\s]* # We just match everything, except for whitespace
|
|
||||||
# since we are only testing for strict identity.
|
|
||||||
)
|
|
||||||
|
|
|
||||||
(?:
|
|
||||||
# The (non)equality operators allow for wild card and local
|
|
||||||
# versions to be specified so we have to define these two
|
|
||||||
# operators separately to enable that.
|
|
||||||
(?<===|!=) # Only match for equals and not equals
|
|
||||||
|
|
||||||
\s*
|
|
||||||
v?
|
|
||||||
(?:[0-9]+!)? # epoch
|
|
||||||
[0-9]+(?:\.[0-9]+)* # release
|
|
||||||
(?: # pre release
|
|
||||||
[-_\.]?
|
|
||||||
(a|b|c|rc|alpha|beta|pre|preview)
|
|
||||||
[-_\.]?
|
|
||||||
[0-9]*
|
|
||||||
)?
|
|
||||||
(?: # post release
|
|
||||||
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
|
|
||||||
)?
|
|
||||||
|
|
||||||
# You cannot use a wild card and a dev or local version
|
|
||||||
# together so group them with a | and make them optional.
|
|
||||||
(?:
|
|
||||||
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
|
|
||||||
(?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
|
|
||||||
|
|
|
||||||
\.\* # Wild card syntax of .*
|
|
||||||
)?
|
|
||||||
)
|
|
||||||
|
|
|
||||||
(?:
|
|
||||||
# The compatible operator requires at least two digits in the
|
|
||||||
# release segment.
|
|
||||||
(?<=~=) # Only match for the compatible operator
|
|
||||||
|
|
||||||
\s*
|
|
||||||
v?
|
|
||||||
(?:[0-9]+!)? # epoch
|
|
||||||
[0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *)
|
|
||||||
(?: # pre release
|
|
||||||
[-_\.]?
|
|
||||||
(a|b|c|rc|alpha|beta|pre|preview)
|
|
||||||
[-_\.]?
|
|
||||||
[0-9]*
|
|
||||||
)?
|
|
||||||
(?: # post release
|
|
||||||
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
|
|
||||||
)?
|
|
||||||
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
|
|
||||||
)
|
|
||||||
|
|
|
||||||
(?:
|
|
||||||
# All other operators only allow a sub set of what the
|
|
||||||
# (non)equality operators do. Specifically they do not allow
|
|
||||||
# local versions to be specified nor do they allow the prefix
|
|
||||||
# matching wild cards.
|
|
||||||
(?<!==|!=|~=) # We have special cases for these
|
|
||||||
# operators so we want to make sure they
|
|
||||||
# don't match here.
|
|
||||||
|
|
||||||
\s*
|
|
||||||
v?
|
|
||||||
(?:[0-9]+!)? # epoch
|
|
||||||
[0-9]+(?:\.[0-9]+)* # release
|
|
||||||
(?: # pre release
|
|
||||||
[-_\.]?
|
|
||||||
(a|b|c|rc|alpha|beta|pre|preview)
|
|
||||||
[-_\.]?
|
|
||||||
[0-9]*
|
|
||||||
)?
|
|
||||||
(?: # post release
|
|
||||||
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
|
|
||||||
)?
|
|
||||||
(?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release
|
|
||||||
)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
_regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
|
|
||||||
|
|
||||||
_operators = {
|
|
||||||
"~=": "compatible",
|
|
||||||
"==": "equal",
|
|
||||||
"!=": "not_equal",
|
|
||||||
"<=": "less_than_equal",
|
|
||||||
">=": "greater_than_equal",
|
|
||||||
"<": "less_than",
|
|
||||||
">": "greater_than",
|
|
||||||
"===": "arbitrary",
|
|
||||||
}
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_compatible(self, prospective: ParsedVersion, 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
|
|
||||||
# implementing it ourselves. The only thing we need to do is construct
|
|
||||||
# the other specifiers.
|
|
||||||
|
|
||||||
# We want everything but the last item in the version, but we want to
|
|
||||||
# ignore suffix segments.
|
|
||||||
prefix = ".".join(
|
|
||||||
list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add the prefix notation to the end of our string
|
|
||||||
prefix += ".*"
|
|
||||||
|
|
||||||
return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
|
|
||||||
prospective, prefix
|
|
||||||
)
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_equal(self, prospective: ParsedVersion, 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.
|
|
||||||
prospective = Version(prospective.public)
|
|
||||||
# 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_spec = _version_split(spec[:-2]) # Remove the trailing .*
|
|
||||||
|
|
||||||
# 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_prospective = _version_split(str(prospective))
|
|
||||||
|
|
||||||
# Shorten the prospective version to be the same length as the spec
|
|
||||||
# so that we can determine if the specifier is a prefix of the
|
|
||||||
# prospective version or not.
|
|
||||||
shortened_prospective = split_prospective[: len(split_spec)]
|
|
||||||
|
|
||||||
# Pad out our two sides with zeros so that they both equal the same
|
|
||||||
# length.
|
|
||||||
padded_spec, padded_prospective = _pad_version(
|
|
||||||
split_spec, shortened_prospective
|
|
||||||
)
|
|
||||||
|
|
||||||
return padded_prospective == padded_spec
|
|
||||||
else:
|
|
||||||
# Convert our spec string into a Version
|
|
||||||
spec_version = Version(spec)
|
|
||||||
|
|
||||||
# If the specifier does not have a local segment, then we want to
|
|
||||||
# act as if the prospective version also does not have a local
|
|
||||||
# segment.
|
|
||||||
if not spec_version.local:
|
|
||||||
prospective = Version(prospective.public)
|
|
||||||
|
|
||||||
return prospective == spec_version
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_not_equal(self, prospective: ParsedVersion, spec: str) -> bool:
|
|
||||||
return not self._compare_equal(prospective, spec)
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_less_than_equal(self, prospective: ParsedVersion, 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)
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_greater_than_equal(
|
|
||||||
self, prospective: ParsedVersion, 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)
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_less_than(self, prospective: ParsedVersion, 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)
|
|
||||||
|
|
||||||
# Check to see if the prospective version is less than the spec
|
|
||||||
# version. If it's not we can short circuit and just return False now
|
|
||||||
# instead of doing extra unneeded work.
|
|
||||||
if not prospective < spec:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# This special case is here so that, unless the specifier itself
|
|
||||||
# includes is a pre-release version, that we do not accept pre-release
|
|
||||||
# versions for the version mentioned in the specifier (e.g. <3.1 should
|
|
||||||
# not match 3.1.dev0, but should match 3.0.dev0).
|
|
||||||
if not spec.is_prerelease and prospective.is_prerelease:
|
|
||||||
if Version(prospective.base_version) == Version(spec.base_version):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If we've gotten to here, it means that prospective version is both
|
|
||||||
# less than the spec version *and* it's not a pre-release of the same
|
|
||||||
# version in the spec.
|
|
||||||
return True
|
|
||||||
|
|
||||||
@_require_version_compare
|
|
||||||
def _compare_greater_than(self, prospective: ParsedVersion, 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)
|
|
||||||
|
|
||||||
# Check to see if the prospective version is greater than the spec
|
|
||||||
# version. If it's not we can short circuit and just return False now
|
|
||||||
# instead of doing extra unneeded work.
|
|
||||||
if not prospective > spec:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# This special case is here so that, unless the specifier itself
|
|
||||||
# includes is a post-release version, that we do not accept
|
|
||||||
# post-release versions for the version mentioned in the specifier
|
|
||||||
# (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0).
|
|
||||||
if not spec.is_postrelease and prospective.is_postrelease:
|
|
||||||
if Version(prospective.base_version) == Version(spec.base_version):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ensure that we do not allow a local version of the version mentioned
|
|
||||||
# in the specifier, which is technically greater than, to match.
|
|
||||||
if prospective.local is not None:
|
|
||||||
if Version(prospective.base_version) == Version(spec.base_version):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If we've gotten to here, it means that prospective version is both
|
|
||||||
# greater than the spec version *and* it's not a pre-release of the
|
|
||||||
# same version in the spec.
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
|
|
||||||
return str(prospective).lower() == str(spec).lower()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def prereleases(self) -> bool:
|
|
||||||
|
|
||||||
# If there is an explicit prereleases set for this, then we'll just
|
|
||||||
# blindly use that.
|
|
||||||
if self._prereleases is not None:
|
|
||||||
return self._prereleases
|
|
||||||
|
|
||||||
# Look at all of our specifiers and determine if they are inclusive
|
|
||||||
# operators, and if they are if they are including an explicit
|
|
||||||
# prerelease.
|
|
||||||
operator, version = self._spec
|
|
||||||
if operator in ["==", ">=", "<=", "~=", "==="]:
|
|
||||||
# The == specifier can include a trailing .*, if it does we
|
|
||||||
# want to remove before parsing.
|
|
||||||
if operator == "==" and version.endswith(".*"):
|
|
||||||
version = version[:-2]
|
|
||||||
|
|
||||||
# Parse the version, and if it is a pre-release than this
|
|
||||||
# specifier allows pre-releases.
|
|
||||||
if parse(version).is_prerelease:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@prereleases.setter
|
|
||||||
def prereleases(self, value: bool) -> None:
|
|
||||||
self._prereleases = value
|
|
||||||
|
|
||||||
|
|
||||||
_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
|
|
||||||
|
|
||||||
|
|
||||||
def _version_split(version: str) -> List[str]:
|
|
||||||
result: List[str] = []
|
|
||||||
for item in version.split("."):
|
|
||||||
match = _prefix_regex.search(item)
|
|
||||||
if match:
|
|
||||||
result.extend(match.groups())
|
|
||||||
else:
|
|
||||||
result.append(item)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _is_not_suffix(segment: str) -> bool:
|
|
||||||
return not any(
|
|
||||||
segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
|
|
||||||
left_split, right_split = [], []
|
|
||||||
|
|
||||||
# Get the release segment of our versions
|
|
||||||
left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left)))
|
|
||||||
right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
|
|
||||||
|
|
||||||
# Get the rest of our versions
|
|
||||||
left_split.append(left[len(left_split[0]) :])
|
|
||||||
right_split.append(right[len(right_split[0]) :])
|
|
||||||
|
|
||||||
# Insert our padding
|
|
||||||
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)))
|
|
||||||
|
|
||||||
|
|
||||||
class SpecifierSet(BaseSpecifier):
|
|
||||||
def __init__(
|
|
||||||
self, specifiers: str = "", prereleases: Optional[bool] = None
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
# Split on , to break each individual specifier into it's own item, and
|
|
||||||
# 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 and falling back to a LegacySpecifier.
|
|
||||||
parsed: Set[_IndividualSpecifier] = set()
|
|
||||||
for specifier in split_specifiers:
|
|
||||||
try:
|
|
||||||
parsed.add(Specifier(specifier))
|
|
||||||
except InvalidSpecifier:
|
|
||||||
parsed.add(LegacySpecifier(specifier))
|
|
||||||
|
|
||||||
# Turn our parsed specifiers into a frozen set and save them for later.
|
|
||||||
self._specs = frozenset(parsed)
|
|
||||||
|
|
||||||
# Store our prereleases value so we can use it later to determine if
|
|
||||||
# we accept prereleases or not.
|
|
||||||
self._prereleases = prereleases
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
pre = (
|
|
||||||
f", prereleases={self.prereleases!r}"
|
|
||||||
if self._prereleases is not None
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"<SpecifierSet({str(self)!r}{pre})>"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return ",".join(sorted(str(s) for s in self._specs))
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self._specs)
|
|
||||||
|
|
||||||
def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
|
|
||||||
if isinstance(other, str):
|
|
||||||
other = SpecifierSet(other)
|
|
||||||
elif not isinstance(other, SpecifierSet):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
specifier = SpecifierSet()
|
|
||||||
specifier._specs = frozenset(self._specs | other._specs)
|
|
||||||
|
|
||||||
if self._prereleases is None and other._prereleases is not None:
|
|
||||||
specifier._prereleases = other._prereleases
|
|
||||||
elif self._prereleases is not None and other._prereleases is None:
|
|
||||||
specifier._prereleases = self._prereleases
|
|
||||||
elif self._prereleases == other._prereleases:
|
|
||||||
specifier._prereleases = self._prereleases
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"Cannot combine SpecifierSets with True and False prerelease "
|
|
||||||
"overrides."
|
|
||||||
)
|
|
||||||
|
|
||||||
return specifier
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if isinstance(other, (str, _IndividualSpecifier)):
|
|
||||||
other = SpecifierSet(str(other))
|
|
||||||
elif not isinstance(other, SpecifierSet):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._specs == other._specs
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._specs)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[_IndividualSpecifier]:
|
|
||||||
return iter(self._specs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def prereleases(self) -> Optional[bool]:
|
|
||||||
|
|
||||||
# If we have been given an explicit prerelease modifier, then we'll
|
|
||||||
# pass that through here.
|
|
||||||
if self._prereleases is not None:
|
|
||||||
return self._prereleases
|
|
||||||
|
|
||||||
# If we don't have any specifiers, and we don't have a forced value,
|
|
||||||
# then we'll just return None since we don't know if this should have
|
|
||||||
# pre-releases or not.
|
|
||||||
if not self._specs:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Otherwise we'll see if any of the given specifiers accept
|
|
||||||
# prereleases, if any of them do we'll return True, otherwise False.
|
|
||||||
return any(s.prereleases for s in self._specs)
|
|
||||||
|
|
||||||
@prereleases.setter
|
|
||||||
def prereleases(self, value: bool) -> None:
|
|
||||||
self._prereleases = value
|
|
||||||
|
|
||||||
def __contains__(self, item: UnparsedVersion) -> bool:
|
|
||||||
return self.contains(item)
|
|
||||||
|
|
||||||
def contains(
|
|
||||||
self, item: UnparsedVersion, prereleases: Optional[bool] = None
|
|
||||||
) -> bool:
|
|
||||||
|
|
||||||
# Ensure that our item is a Version or LegacyVersion instance.
|
|
||||||
if not isinstance(item, (LegacyVersion, Version)):
|
|
||||||
item = parse(item)
|
|
||||||
|
|
||||||
# Determine if we're forcing a prerelease or not, if we're not forcing
|
|
||||||
# one for this particular filter call, then we'll use whatever the
|
|
||||||
# SpecifierSet thinks for whether or not we should support prereleases.
|
|
||||||
if prereleases is None:
|
|
||||||
prereleases = self.prereleases
|
|
||||||
|
|
||||||
# We can determine if we're going to allow pre-releases by looking to
|
|
||||||
# see if any of the underlying items supports them. If none of them do
|
|
||||||
# and this item is a pre-release then we do not allow it and we can
|
|
||||||
# short circuit that here.
|
|
||||||
# Note: This means that 1.0.dev1 would not be contained in something
|
|
||||||
# like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0
|
|
||||||
if not prereleases and item.is_prerelease:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# We simply dispatch to the underlying specs here to make sure that the
|
|
||||||
# given version is contained within all of them.
|
|
||||||
# Note: This use of all() here means that an empty set of specifiers
|
|
||||||
# will always return True, this is an explicit design decision.
|
|
||||||
return all(s.contains(item, prereleases=prereleases) for s in self._specs)
|
|
||||||
|
|
||||||
def filter(
|
|
||||||
self, iterable: Iterable[VersionTypeVar], prereleases: Optional[bool] = None
|
|
||||||
) -> Iterable[VersionTypeVar]:
|
|
||||||
|
|
||||||
# Determine if we're forcing a prerelease or not, if we're not forcing
|
|
||||||
# one for this particular filter call, then we'll use whatever the
|
|
||||||
# SpecifierSet thinks for whether or not we should support prereleases.
|
|
||||||
if prereleases is None:
|
|
||||||
prereleases = self.prereleases
|
|
||||||
|
|
||||||
# If we have any specifiers, then we want to wrap our iterable in the
|
|
||||||
# filter method for each one, this will act as a logical AND amongst
|
|
||||||
# each specifier.
|
|
||||||
if self._specs:
|
|
||||||
for spec in self._specs:
|
|
||||||
iterable = spec.filter(iterable, prereleases=bool(prereleases))
|
|
||||||
return iterable
|
|
||||||
# If we do not have any specifiers, then we need to have a rough filter
|
|
||||||
# which will filter out any pre-releases, unless there are no final
|
|
||||||
# releases, and which will filter out LegacyVersion in general.
|
|
||||||
else:
|
|
||||||
filtered: List[VersionTypeVar] = []
|
|
||||||
found_prereleases: List[VersionTypeVar] = []
|
|
||||||
|
|
||||||
item: UnparsedVersion
|
|
||||||
parsed_version: Union[Version, LegacyVersion]
|
|
||||||
|
|
||||||
for item in iterable:
|
|
||||||
# Ensure that we some kind of Version class for this item.
|
|
||||||
if not isinstance(item, (LegacyVersion, Version)):
|
|
||||||
parsed_version = parse(item)
|
|
||||||
else:
|
|
||||||
parsed_version = item
|
|
||||||
|
|
||||||
# Filter out any item which is parsed as a LegacyVersion
|
|
||||||
if isinstance(parsed_version, LegacyVersion):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Store any item which is a pre-release for later unless we've
|
|
||||||
# already found a final version or we are accepting prereleases
|
|
||||||
if parsed_version.is_prerelease and not prereleases:
|
|
||||||
if not filtered:
|
|
||||||
found_prereleases.append(item)
|
|
||||||
else:
|
|
||||||
filtered.append(item)
|
|
||||||
|
|
||||||
# If we've found no items except for pre-releases, then we'll go
|
|
||||||
# ahead and use the pre-releases
|
|
||||||
if not filtered and found_prereleases and prereleases is None:
|
|
||||||
return found_prereleases
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
@@ -1,487 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import platform
|
|
||||||
import sys
|
|
||||||
import sysconfig
|
|
||||||
from importlib.machinery import EXTENSION_SUFFIXES
|
|
||||||
from typing import (
|
|
||||||
Dict,
|
|
||||||
FrozenSet,
|
|
||||||
Iterable,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Sequence,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
cast,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import _manylinux, _musllinux
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
PythonVersion = Sequence[int]
|
|
||||||
MacVersion = Tuple[int, int]
|
|
||||||
|
|
||||||
INTERPRETER_SHORT_NAMES: Dict[str, str] = {
|
|
||||||
"python": "py", # Generic.
|
|
||||||
"cpython": "cp",
|
|
||||||
"pypy": "pp",
|
|
||||||
"ironpython": "ip",
|
|
||||||
"jython": "jy",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32
|
|
||||||
|
|
||||||
|
|
||||||
class Tag:
|
|
||||||
"""
|
|
||||||
A representation of the tag triple for a wheel.
|
|
||||||
|
|
||||||
Instances are considered immutable and thus are hashable. Equality checking
|
|
||||||
is also supported.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__slots__ = ["_interpreter", "_abi", "_platform", "_hash"]
|
|
||||||
|
|
||||||
def __init__(self, interpreter: str, abi: str, platform: str) -> None:
|
|
||||||
self._interpreter = interpreter.lower()
|
|
||||||
self._abi = abi.lower()
|
|
||||||
self._platform = platform.lower()
|
|
||||||
# The __hash__ of every single element in a Set[Tag] will be evaluated each time
|
|
||||||
# that a set calls its `.disjoint()` method, which may be called hundreds of
|
|
||||||
# times when scanning a page of links for packages with tags matching that
|
|
||||||
# Set[Tag]. Pre-computing the value here produces significant speedups for
|
|
||||||
# downstream consumers.
|
|
||||||
self._hash = hash((self._interpreter, self._abi, self._platform))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def interpreter(self) -> str:
|
|
||||||
return self._interpreter
|
|
||||||
|
|
||||||
@property
|
|
||||||
def abi(self) -> str:
|
|
||||||
return self._abi
|
|
||||||
|
|
||||||
@property
|
|
||||||
def platform(self) -> str:
|
|
||||||
return self._platform
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, Tag):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return (
|
|
||||||
(self._hash == other._hash) # Short-circuit ASAP for perf reasons.
|
|
||||||
and (self._platform == other._platform)
|
|
||||||
and (self._abi == other._abi)
|
|
||||||
and (self._interpreter == other._interpreter)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return self._hash
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self._interpreter}-{self._abi}-{self._platform}"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<{self} @ {id(self)}>"
|
|
||||||
|
|
||||||
|
|
||||||
def parse_tag(tag: str) -> FrozenSet[Tag]:
|
|
||||||
"""
|
|
||||||
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
|
|
||||||
|
|
||||||
Returning a set is required due to the possibility that the tag is a
|
|
||||||
compressed tag set.
|
|
||||||
"""
|
|
||||||
tags = set()
|
|
||||||
interpreters, abis, platforms = tag.split("-")
|
|
||||||
for interpreter in interpreters.split("."):
|
|
||||||
for abi in abis.split("."):
|
|
||||||
for platform_ in platforms.split("."):
|
|
||||||
tags.add(Tag(interpreter, abi, platform_))
|
|
||||||
return frozenset(tags)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
|
|
||||||
value = 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
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_string(string: str) -> str:
|
|
||||||
return string.replace(".", "_").replace("-", "_")
|
|
||||||
|
|
||||||
|
|
||||||
def _abi3_applies(python_version: PythonVersion) -> bool:
|
|
||||||
"""
|
|
||||||
Determine if the Python version supports abi3.
|
|
||||||
|
|
||||||
PEP 384 was first implemented in Python 3.2.
|
|
||||||
"""
|
|
||||||
return len(python_version) > 1 and tuple(python_version) >= (3, 2)
|
|
||||||
|
|
||||||
|
|
||||||
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 = ""
|
|
||||||
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
|
|
||||||
# extension modules is the best option.
|
|
||||||
# https://github.com/pypa/pip/issues/3383#issuecomment-173267692
|
|
||||||
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, 8):
|
|
||||||
with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
|
|
||||||
if with_pymalloc or with_pymalloc is None:
|
|
||||||
pymalloc = "m"
|
|
||||||
if py_version < (3, 3):
|
|
||||||
unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
|
|
||||||
if unicode_size == 4 or (
|
|
||||||
unicode_size is None and sys.maxunicode == 0x10FFFF
|
|
||||||
):
|
|
||||||
ucs4 = "u"
|
|
||||||
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
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return abis
|
|
||||||
|
|
||||||
|
|
||||||
def cpython_tags(
|
|
||||||
python_version: Optional[PythonVersion] = None,
|
|
||||||
abis: Optional[Iterable[str]] = None,
|
|
||||||
platforms: Optional[Iterable[str]] = None,
|
|
||||||
*,
|
|
||||||
warn: bool = False,
|
|
||||||
) -> Iterator[Tag]:
|
|
||||||
"""
|
|
||||||
Yields the tags for a CPython interpreter.
|
|
||||||
|
|
||||||
The tags consist of:
|
|
||||||
- cp<python_version>-<abi>-<platform>
|
|
||||||
- cp<python_version>-abi3-<platform>
|
|
||||||
- cp<python_version>-none-<platform>
|
|
||||||
- cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2.
|
|
||||||
|
|
||||||
If python_version only specifies a major version then user-provided ABIs and
|
|
||||||
the 'none' ABItag will be used.
|
|
||||||
|
|
||||||
If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
|
|
||||||
their normal position and not at the beginning.
|
|
||||||
"""
|
|
||||||
if not python_version:
|
|
||||||
python_version = sys.version_info[:2]
|
|
||||||
|
|
||||||
interpreter = f"cp{_version_nodot(python_version[:2])}"
|
|
||||||
|
|
||||||
if abis is None:
|
|
||||||
if len(python_version) > 1:
|
|
||||||
abis = _cpython_abis(python_version, warn)
|
|
||||||
else:
|
|
||||||
abis = []
|
|
||||||
abis = list(abis)
|
|
||||||
# 'abi3' and 'none' are explicitly handled later.
|
|
||||||
for explicit_abi in ("abi3", "none"):
|
|
||||||
try:
|
|
||||||
abis.remove(explicit_abi)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
platforms = list(platforms or platform_tags())
|
|
||||||
for abi in abis:
|
|
||||||
for platform_ in platforms:
|
|
||||||
yield Tag(interpreter, abi, platform_)
|
|
||||||
if _abi3_applies(python_version):
|
|
||||||
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):
|
|
||||||
for minor_version in range(python_version[1] - 1, 1, -1):
|
|
||||||
for platform_ in platforms:
|
|
||||||
interpreter = "cp{version}".format(
|
|
||||||
version=_version_nodot((python_version[0], minor_version))
|
|
||||||
)
|
|
||||||
yield Tag(interpreter, "abi3", platform_)
|
|
||||||
|
|
||||||
|
|
||||||
def _generic_abi() -> Iterator[str]:
|
|
||||||
abi = sysconfig.get_config_var("SOABI")
|
|
||||||
if abi:
|
|
||||||
yield _normalize_string(abi)
|
|
||||||
|
|
||||||
|
|
||||||
def generic_tags(
|
|
||||||
interpreter: Optional[str] = None,
|
|
||||||
abis: Optional[Iterable[str]] = None,
|
|
||||||
platforms: Optional[Iterable[str]] = None,
|
|
||||||
*,
|
|
||||||
warn: bool = False,
|
|
||||||
) -> Iterator[Tag]:
|
|
||||||
"""
|
|
||||||
Yields the tags for a generic interpreter.
|
|
||||||
|
|
||||||
The tags consist of:
|
|
||||||
- <interpreter>-<abi>-<platform>
|
|
||||||
|
|
||||||
The "none" ABI will be added if it was not explicitly provided.
|
|
||||||
"""
|
|
||||||
if not interpreter:
|
|
||||||
interp_name = interpreter_name()
|
|
||||||
interp_version = interpreter_version(warn=warn)
|
|
||||||
interpreter = "".join([interp_name, interp_version])
|
|
||||||
if abis is None:
|
|
||||||
abis = _generic_abi()
|
|
||||||
platforms = list(platforms or platform_tags())
|
|
||||||
abis = list(abis)
|
|
||||||
if "none" not in abis:
|
|
||||||
abis.append("none")
|
|
||||||
for abi in abis:
|
|
||||||
for platform_ in platforms:
|
|
||||||
yield Tag(interpreter, abi, platform_)
|
|
||||||
|
|
||||||
|
|
||||||
def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
Yields Python versions in descending order.
|
|
||||||
|
|
||||||
After the latest version, the major-only version will be yielded, and then
|
|
||||||
all previous versions of that major version.
|
|
||||||
"""
|
|
||||||
if len(py_version) > 1:
|
|
||||||
yield f"py{_version_nodot(py_version[:2])}"
|
|
||||||
yield f"py{py_version[0]}"
|
|
||||||
if len(py_version) > 1:
|
|
||||||
for minor in range(py_version[1] - 1, -1, -1):
|
|
||||||
yield f"py{_version_nodot((py_version[0], minor))}"
|
|
||||||
|
|
||||||
|
|
||||||
def compatible_tags(
|
|
||||||
python_version: Optional[PythonVersion] = None,
|
|
||||||
interpreter: Optional[str] = None,
|
|
||||||
platforms: Optional[Iterable[str]] = None,
|
|
||||||
) -> Iterator[Tag]:
|
|
||||||
"""
|
|
||||||
Yields the sequence of tags that are compatible with a specific version of Python.
|
|
||||||
|
|
||||||
The tags consist of:
|
|
||||||
- py*-none-<platform>
|
|
||||||
- <interpreter>-none-any # ... if `interpreter` is provided.
|
|
||||||
- py*-none-any
|
|
||||||
"""
|
|
||||||
if not python_version:
|
|
||||||
python_version = sys.version_info[:2]
|
|
||||||
platforms = list(platforms or platform_tags())
|
|
||||||
for version in _py_interpreter_range(python_version):
|
|
||||||
for platform_ in platforms:
|
|
||||||
yield Tag(version, "none", platform_)
|
|
||||||
if interpreter:
|
|
||||||
yield Tag(interpreter, "none", "any")
|
|
||||||
for version in _py_interpreter_range(python_version):
|
|
||||||
yield Tag(version, "none", "any")
|
|
||||||
|
|
||||||
|
|
||||||
def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
|
|
||||||
if not is_32bit:
|
|
||||||
return arch
|
|
||||||
|
|
||||||
if arch.startswith("ppc"):
|
|
||||||
return "ppc"
|
|
||||||
|
|
||||||
return "i386"
|
|
||||||
|
|
||||||
|
|
||||||
def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
|
|
||||||
formats = [cpu_arch]
|
|
||||||
if cpu_arch == "x86_64":
|
|
||||||
if version < (10, 4):
|
|
||||||
return []
|
|
||||||
formats.extend(["intel", "fat64", "fat32"])
|
|
||||||
|
|
||||||
elif cpu_arch == "i386":
|
|
||||||
if version < (10, 4):
|
|
||||||
return []
|
|
||||||
formats.extend(["intel", "fat32", "fat"])
|
|
||||||
|
|
||||||
elif cpu_arch == "ppc64":
|
|
||||||
# TODO: Need to care about 32-bit PPC for ppc64 through 10.2?
|
|
||||||
if version > (10, 5) or version < (10, 4):
|
|
||||||
return []
|
|
||||||
formats.append("fat64")
|
|
||||||
|
|
||||||
elif cpu_arch == "ppc":
|
|
||||||
if version > (10, 6):
|
|
||||||
return []
|
|
||||||
formats.extend(["fat32", "fat"])
|
|
||||||
|
|
||||||
if cpu_arch in {"arm64", "x86_64"}:
|
|
||||||
formats.append("universal2")
|
|
||||||
|
|
||||||
if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}:
|
|
||||||
formats.append("universal")
|
|
||||||
|
|
||||||
return formats
|
|
||||||
|
|
||||||
|
|
||||||
def mac_platforms(
|
|
||||||
version: Optional[MacVersion] = None, arch: Optional[str] = None
|
|
||||||
) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
Yields the platform tags for a macOS system.
|
|
||||||
|
|
||||||
The `version` parameter is a two-item tuple specifying the macOS version to
|
|
||||||
generate platform tags for. The `arch` parameter is the CPU architecture to
|
|
||||||
generate platform tags for. Both parameters default to the appropriate value
|
|
||||||
for the current system.
|
|
||||||
"""
|
|
||||||
version_str, _, cpu_arch = platform.mac_ver()
|
|
||||||
if version is None:
|
|
||||||
version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2])))
|
|
||||||
else:
|
|
||||||
version = version
|
|
||||||
if arch is None:
|
|
||||||
arch = _mac_arch(cpu_arch)
|
|
||||||
else:
|
|
||||||
arch = arch
|
|
||||||
|
|
||||||
if (10, 0) <= version and version < (11, 0):
|
|
||||||
# Prior to Mac OS 11, each yearly release of Mac OS bumped the
|
|
||||||
# "minor" version number. The major version was always 10.
|
|
||||||
for minor_version in range(version[1], -1, -1):
|
|
||||||
compat_version = 10, minor_version
|
|
||||||
binary_formats = _mac_binary_formats(compat_version, arch)
|
|
||||||
for binary_format in binary_formats:
|
|
||||||
yield "macosx_{major}_{minor}_{binary_format}".format(
|
|
||||||
major=10, minor=minor_version, binary_format=binary_format
|
|
||||||
)
|
|
||||||
|
|
||||||
if version >= (11, 0):
|
|
||||||
# Starting with Mac OS 11, each yearly release bumps the major version
|
|
||||||
# number. The minor versions are now the midyear updates.
|
|
||||||
for major_version in range(version[0], 10, -1):
|
|
||||||
compat_version = major_version, 0
|
|
||||||
binary_formats = _mac_binary_formats(compat_version, arch)
|
|
||||||
for binary_format in binary_formats:
|
|
||||||
yield "macosx_{major}_{minor}_{binary_format}".format(
|
|
||||||
major=major_version, minor=0, binary_format=binary_format
|
|
||||||
)
|
|
||||||
|
|
||||||
if version >= (11, 0):
|
|
||||||
# Mac OS 11 on x86_64 is compatible with binaries from previous releases.
|
|
||||||
# Arm64 support was introduced in 11.0, so no Arm binaries from previous
|
|
||||||
# releases exist.
|
|
||||||
#
|
|
||||||
# However, the "universal2" binary format can have a
|
|
||||||
# macOS version earlier than 11.0 when the x86_64 part of the binary supports
|
|
||||||
# that version of macOS.
|
|
||||||
if arch == "x86_64":
|
|
||||||
for minor_version in range(16, 3, -1):
|
|
||||||
compat_version = 10, minor_version
|
|
||||||
binary_formats = _mac_binary_formats(compat_version, arch)
|
|
||||||
for binary_format in binary_formats:
|
|
||||||
yield "macosx_{major}_{minor}_{binary_format}".format(
|
|
||||||
major=compat_version[0],
|
|
||||||
minor=compat_version[1],
|
|
||||||
binary_format=binary_format,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
for minor_version in range(16, 3, -1):
|
|
||||||
compat_version = 10, minor_version
|
|
||||||
binary_format = "universal2"
|
|
||||||
yield "macosx_{major}_{minor}_{binary_format}".format(
|
|
||||||
major=compat_version[0],
|
|
||||||
minor=compat_version[1],
|
|
||||||
binary_format=binary_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
|
|
||||||
linux = _normalize_string(sysconfig.get_platform())
|
|
||||||
if is_32bit:
|
|
||||||
if linux == "linux_x86_64":
|
|
||||||
linux = "linux_i686"
|
|
||||||
elif linux == "linux_aarch64":
|
|
||||||
linux = "linux_armv7l"
|
|
||||||
_, arch = linux.split("_", 1)
|
|
||||||
yield from _manylinux.platform_tags(linux, arch)
|
|
||||||
yield from _musllinux.platform_tags(arch)
|
|
||||||
yield linux
|
|
||||||
|
|
||||||
|
|
||||||
def _generic_platforms() -> Iterator[str]:
|
|
||||||
yield _normalize_string(sysconfig.get_platform())
|
|
||||||
|
|
||||||
|
|
||||||
def platform_tags() -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
Provides the platform tags for this installation.
|
|
||||||
"""
|
|
||||||
if platform.system() == "Darwin":
|
|
||||||
return mac_platforms()
|
|
||||||
elif platform.system() == "Linux":
|
|
||||||
return _linux_platforms()
|
|
||||||
else:
|
|
||||||
return _generic_platforms()
|
|
||||||
|
|
||||||
|
|
||||||
def interpreter_name() -> str:
|
|
||||||
"""
|
|
||||||
Returns the name of the running interpreter.
|
|
||||||
"""
|
|
||||||
name = sys.implementation.name
|
|
||||||
return INTERPRETER_SHORT_NAMES.get(name) or name
|
|
||||||
|
|
||||||
|
|
||||||
def interpreter_version(*, warn: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
Returns the version of the running interpreter.
|
|
||||||
"""
|
|
||||||
version = _get_config_var("py_version_nodot", warn=warn)
|
|
||||||
if version:
|
|
||||||
version = str(version)
|
|
||||||
else:
|
|
||||||
version = _version_nodot(sys.version_info[:2])
|
|
||||||
return version
|
|
||||||
|
|
||||||
|
|
||||||
def _version_nodot(version: PythonVersion) -> str:
|
|
||||||
return "".join(map(str, version))
|
|
||||||
|
|
||||||
|
|
||||||
def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
|
|
||||||
"""
|
|
||||||
Returns the sequence of tag triples for the running interpreter.
|
|
||||||
|
|
||||||
The order of the sequence corresponds to priority order for the
|
|
||||||
interpreter, from most to least important.
|
|
||||||
"""
|
|
||||||
|
|
||||||
interp_name = interpreter_name()
|
|
||||||
if interp_name == "cp":
|
|
||||||
yield from cpython_tags(warn=warn)
|
|
||||||
else:
|
|
||||||
yield from generic_tags()
|
|
||||||
|
|
||||||
if interp_name == "pp":
|
|
||||||
yield from compatible_tags(interpreter="pp3")
|
|
||||||
else:
|
|
||||||
yield from compatible_tags()
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import FrozenSet, NewType, Tuple, Union, cast
|
|
||||||
|
|
||||||
from .tags import Tag, parse_tag
|
|
||||||
from .version import InvalidVersion, Version
|
|
||||||
|
|
||||||
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
|
||||||
NormalizedName = NewType("NormalizedName", str)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidWheelFilename(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid wheel filename was found, users should refer to PEP 427.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSdistFilename(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid sdist filename was found, users should refer to the packaging user guide.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
_canonicalize_regex = re.compile(r"[-_.]+")
|
|
||||||
# PEP 427: The build number must start with a digit.
|
|
||||||
_build_tag_regex = re.compile(r"(\d+)(.*)")
|
|
||||||
|
|
||||||
|
|
||||||
def canonicalize_name(name: str) -> NormalizedName:
|
|
||||||
# This is taken from PEP 503.
|
|
||||||
value = _canonicalize_regex.sub("-", name).lower()
|
|
||||||
return cast(NormalizedName, value)
|
|
||||||
|
|
||||||
|
|
||||||
def canonicalize_version(version: Union[Version, str]) -> str:
|
|
||||||
"""
|
|
||||||
This is very similar to Version.__str__, but has one subtle difference
|
|
||||||
with the way it handles the release segment.
|
|
||||||
"""
|
|
||||||
if isinstance(version, str):
|
|
||||||
try:
|
|
||||||
parsed = Version(version)
|
|
||||||
except InvalidVersion:
|
|
||||||
# Legacy versions cannot be normalized
|
|
||||||
return version
|
|
||||||
else:
|
|
||||||
parsed = version
|
|
||||||
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
# Epoch
|
|
||||||
if parsed.epoch != 0:
|
|
||||||
parts.append(f"{parsed.epoch}!")
|
|
||||||
|
|
||||||
# Release segment
|
|
||||||
# NB: This strips trailing '.0's to normalize
|
|
||||||
parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in parsed.release)))
|
|
||||||
|
|
||||||
# Pre-release
|
|
||||||
if parsed.pre is not None:
|
|
||||||
parts.append("".join(str(x) for x in parsed.pre))
|
|
||||||
|
|
||||||
# Post-release
|
|
||||||
if parsed.post is not None:
|
|
||||||
parts.append(f".post{parsed.post}")
|
|
||||||
|
|
||||||
# Development release
|
|
||||||
if parsed.dev is not None:
|
|
||||||
parts.append(f".dev{parsed.dev}")
|
|
||||||
|
|
||||||
# Local version segment
|
|
||||||
if parsed.local is not None:
|
|
||||||
parts.append(f"+{parsed.local}")
|
|
||||||
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_wheel_filename(
|
|
||||||
filename: str,
|
|
||||||
) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
|
|
||||||
if not filename.endswith(".whl"):
|
|
||||||
raise InvalidWheelFilename(
|
|
||||||
f"Invalid wheel filename (extension must be '.whl'): {filename}"
|
|
||||||
)
|
|
||||||
|
|
||||||
filename = filename[:-4]
|
|
||||||
dashes = filename.count("-")
|
|
||||||
if dashes not in (4, 5):
|
|
||||||
raise InvalidWheelFilename(
|
|
||||||
f"Invalid wheel filename (wrong number of parts): {filename}"
|
|
||||||
)
|
|
||||||
|
|
||||||
parts = filename.split("-", dashes - 2)
|
|
||||||
name_part = parts[0]
|
|
||||||
# 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])
|
|
||||||
if dashes == 5:
|
|
||||||
build_part = parts[2]
|
|
||||||
build_match = _build_tag_regex.match(build_part)
|
|
||||||
if build_match is None:
|
|
||||||
raise InvalidWheelFilename(
|
|
||||||
f"Invalid build number: {build_part} in '{filename}'"
|
|
||||||
)
|
|
||||||
build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
|
|
||||||
else:
|
|
||||||
build = ()
|
|
||||||
tags = parse_tag(parts[-1])
|
|
||||||
return (name, version, build, tags)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
|
|
||||||
if filename.endswith(".tar.gz"):
|
|
||||||
file_stem = filename[: -len(".tar.gz")]
|
|
||||||
elif filename.endswith(".zip"):
|
|
||||||
file_stem = filename[: -len(".zip")]
|
|
||||||
else:
|
|
||||||
raise InvalidSdistFilename(
|
|
||||||
f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
|
|
||||||
f" {filename}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# We are requiring a PEP 440 version, which cannot contain dashes,
|
|
||||||
# so we split on the last dash.
|
|
||||||
name_part, sep, version_part = file_stem.rpartition("-")
|
|
||||||
if not sep:
|
|
||||||
raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
|
|
||||||
|
|
||||||
name = canonicalize_name(name_part)
|
|
||||||
version = Version(version_part)
|
|
||||||
return (name, version)
|
|
||||||
@@ -1,504 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import itertools
|
|
||||||
import re
|
|
||||||
import warnings
|
|
||||||
from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union
|
|
||||||
|
|
||||||
from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
|
|
||||||
|
|
||||||
__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"]
|
|
||||||
|
|
||||||
InfiniteTypes = Union[InfinityType, NegativeInfinityType]
|
|
||||||
PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
|
|
||||||
SubLocalType = Union[InfiniteTypes, int, str]
|
|
||||||
LocalType = Union[
|
|
||||||
NegativeInfinityType,
|
|
||||||
Tuple[
|
|
||||||
Union[
|
|
||||||
SubLocalType,
|
|
||||||
Tuple[SubLocalType, str],
|
|
||||||
Tuple[NegativeInfinityType, SubLocalType],
|
|
||||||
],
|
|
||||||
...,
|
|
||||||
],
|
|
||||||
]
|
|
||||||
CmpKey = Tuple[
|
|
||||||
int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
|
|
||||||
]
|
|
||||||
LegacyCmpKey = Tuple[int, Tuple[str, ...]]
|
|
||||||
VersionComparisonMethod = Callable[
|
|
||||||
[Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool
|
|
||||||
]
|
|
||||||
|
|
||||||
_Version = collections.namedtuple(
|
|
||||||
"_Version", ["epoch", "release", "dev", "pre", "post", "local"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse(version: str) -> Union["LegacyVersion", "Version"]:
|
|
||||||
"""
|
|
||||||
Parse the given version string and return either a :class:`Version` object
|
|
||||||
or a :class:`LegacyVersion` object depending on if the given version is
|
|
||||||
a valid PEP 440 version or a legacy version.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return Version(version)
|
|
||||||
except InvalidVersion:
|
|
||||||
return LegacyVersion(version)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidVersion(ValueError):
|
|
||||||
"""
|
|
||||||
An invalid version was found, users should refer to PEP 440.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _BaseVersion:
|
|
||||||
_key: Union[CmpKey, LegacyCmpKey]
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(self._key)
|
|
||||||
|
|
||||||
# Please keep the duplicated `isinstance` check
|
|
||||||
# in the six comparisons hereunder
|
|
||||||
# unless you find a way to avoid adding overhead function calls.
|
|
||||||
def __lt__(self, other: "_BaseVersion") -> bool:
|
|
||||||
if not isinstance(other, _BaseVersion):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._key < other._key
|
|
||||||
|
|
||||||
def __le__(self, other: "_BaseVersion") -> bool:
|
|
||||||
if not isinstance(other, _BaseVersion):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._key <= other._key
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, _BaseVersion):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._key == other._key
|
|
||||||
|
|
||||||
def __ge__(self, other: "_BaseVersion") -> bool:
|
|
||||||
if not isinstance(other, _BaseVersion):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._key >= other._key
|
|
||||||
|
|
||||||
def __gt__(self, other: "_BaseVersion") -> bool:
|
|
||||||
if not isinstance(other, _BaseVersion):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._key > other._key
|
|
||||||
|
|
||||||
def __ne__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, _BaseVersion):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self._key != other._key
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyVersion(_BaseVersion):
|
|
||||||
def __init__(self, version: str) -> None:
|
|
||||||
self._version = str(version)
|
|
||||||
self._key = _legacy_cmpkey(self._version)
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Creating a LegacyVersion has been deprecated and will be "
|
|
||||||
"removed in the next major release",
|
|
||||||
DeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self._version
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<LegacyVersion('{self}')>"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def public(self) -> str:
|
|
||||||
return self._version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def base_version(self) -> str:
|
|
||||||
return self._version
|
|
||||||
|
|
||||||
@property
|
|
||||||
def epoch(self) -> int:
|
|
||||||
return -1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def release(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pre(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def post(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dev(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def local(self) -> None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_prerelease(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_postrelease(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_devrelease(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
|
|
||||||
|
|
||||||
_legacy_version_replacement_map = {
|
|
||||||
"pre": "c",
|
|
||||||
"preview": "c",
|
|
||||||
"-": "final-",
|
|
||||||
"rc": "c",
|
|
||||||
"dev": "@",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_version_parts(s: str) -> Iterator[str]:
|
|
||||||
for part in _legacy_version_component_re.split(s):
|
|
||||||
part = _legacy_version_replacement_map.get(part, part)
|
|
||||||
|
|
||||||
if not part or part == ".":
|
|
||||||
continue
|
|
||||||
|
|
||||||
if part[:1] in "0123456789":
|
|
||||||
# pad for numeric comparison
|
|
||||||
yield part.zfill(8)
|
|
||||||
else:
|
|
||||||
yield "*" + part
|
|
||||||
|
|
||||||
# ensure that alpha/beta/candidate are before final
|
|
||||||
yield "*final"
|
|
||||||
|
|
||||||
|
|
||||||
def _legacy_cmpkey(version: str) -> LegacyCmpKey:
|
|
||||||
|
|
||||||
# We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
|
|
||||||
# greater than or equal to 0. This will effectively put the LegacyVersion,
|
|
||||||
# which uses the defacto standard originally implemented by setuptools,
|
|
||||||
# as before all PEP 440 versions.
|
|
||||||
epoch = -1
|
|
||||||
|
|
||||||
# This scheme is taken from pkg_resources.parse_version setuptools prior to
|
|
||||||
# it's adoption of the packaging library.
|
|
||||||
parts: List[str] = []
|
|
||||||
for part in _parse_version_parts(version.lower()):
|
|
||||||
if part.startswith("*"):
|
|
||||||
# remove "-" before a prerelease tag
|
|
||||||
if part < "*final":
|
|
||||||
while parts and parts[-1] == "*final-":
|
|
||||||
parts.pop()
|
|
||||||
|
|
||||||
# remove trailing zeros from each series of numeric parts
|
|
||||||
while parts and parts[-1] == "00000000":
|
|
||||||
parts.pop()
|
|
||||||
|
|
||||||
parts.append(part)
|
|
||||||
|
|
||||||
return epoch, tuple(parts)
|
|
||||||
|
|
||||||
|
|
||||||
# Deliberately not anchored to the start and end of the string, to make it
|
|
||||||
# easier for 3rd party code to reuse
|
|
||||||
VERSION_PATTERN = r"""
|
|
||||||
v?
|
|
||||||
(?:
|
|
||||||
(?:(?P<epoch>[0-9]+)!)? # epoch
|
|
||||||
(?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_n>[0-9]+)?
|
|
||||||
)?
|
|
||||||
(?P<post> # post release
|
|
||||||
(?:-(?P<post_n1>[0-9]+))
|
|
||||||
|
|
|
||||||
(?:
|
|
||||||
[-_\.]?
|
|
||||||
(?P<post_l>post|rev|r)
|
|
||||||
[-_\.]?
|
|
||||||
(?P<post_n2>[0-9]+)?
|
|
||||||
)
|
|
||||||
)?
|
|
||||||
(?P<dev> # dev release
|
|
||||||
[-_\.]?
|
|
||||||
(?P<dev_l>dev)
|
|
||||||
[-_\.]?
|
|
||||||
(?P<dev_n>[0-9]+)?
|
|
||||||
)?
|
|
||||||
)
|
|
||||||
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Version(_BaseVersion):
|
|
||||||
|
|
||||||
_regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
|
|
||||||
|
|
||||||
def __init__(self, version: str) -> None:
|
|
||||||
|
|
||||||
# Validate the version and parse it into pieces
|
|
||||||
match = self._regex.search(version)
|
|
||||||
if not match:
|
|
||||||
raise InvalidVersion(f"Invalid version: '{version}'")
|
|
||||||
|
|
||||||
# Store the parsed out pieces of the version
|
|
||||||
self._version = _Version(
|
|
||||||
epoch=int(match.group("epoch")) if match.group("epoch") else 0,
|
|
||||||
release=tuple(int(i) for i in match.group("release").split(".")),
|
|
||||||
pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
|
|
||||||
post=_parse_letter_version(
|
|
||||||
match.group("post_l"), match.group("post_n1") or match.group("post_n2")
|
|
||||||
),
|
|
||||||
dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
|
|
||||||
local=_parse_local_version(match.group("local")),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Generate a key which will be used for sorting
|
|
||||||
self._key = _cmpkey(
|
|
||||||
self._version.epoch,
|
|
||||||
self._version.release,
|
|
||||||
self._version.pre,
|
|
||||||
self._version.post,
|
|
||||||
self._version.dev,
|
|
||||||
self._version.local,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"<Version('{self}')>"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
# Epoch
|
|
||||||
if self.epoch != 0:
|
|
||||||
parts.append(f"{self.epoch}!")
|
|
||||||
|
|
||||||
# Release segment
|
|
||||||
parts.append(".".join(str(x) for x in self.release))
|
|
||||||
|
|
||||||
# Pre-release
|
|
||||||
if self.pre is not None:
|
|
||||||
parts.append("".join(str(x) for x in self.pre))
|
|
||||||
|
|
||||||
# Post-release
|
|
||||||
if self.post is not None:
|
|
||||||
parts.append(f".post{self.post}")
|
|
||||||
|
|
||||||
# Development release
|
|
||||||
if self.dev is not None:
|
|
||||||
parts.append(f".dev{self.dev}")
|
|
||||||
|
|
||||||
# Local version segment
|
|
||||||
if self.local is not None:
|
|
||||||
parts.append(f"+{self.local}")
|
|
||||||
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def epoch(self) -> int:
|
|
||||||
_epoch: int = self._version.epoch
|
|
||||||
return _epoch
|
|
||||||
|
|
||||||
@property
|
|
||||||
def release(self) -> Tuple[int, ...]:
|
|
||||||
_release: Tuple[int, ...] = self._version.release
|
|
||||||
return _release
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pre(self) -> Optional[Tuple[str, int]]:
|
|
||||||
_pre: Optional[Tuple[str, int]] = self._version.pre
|
|
||||||
return _pre
|
|
||||||
|
|
||||||
@property
|
|
||||||
def post(self) -> Optional[int]:
|
|
||||||
return self._version.post[1] if self._version.post else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dev(self) -> Optional[int]:
|
|
||||||
return self._version.dev[1] if self._version.dev else None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def local(self) -> Optional[str]:
|
|
||||||
if self._version.local:
|
|
||||||
return ".".join(str(x) for x in self._version.local)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def public(self) -> str:
|
|
||||||
return str(self).split("+", 1)[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def base_version(self) -> str:
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
# Epoch
|
|
||||||
if self.epoch != 0:
|
|
||||||
parts.append(f"{self.epoch}!")
|
|
||||||
|
|
||||||
# Release segment
|
|
||||||
parts.append(".".join(str(x) for x in self.release))
|
|
||||||
|
|
||||||
return "".join(parts)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_prerelease(self) -> bool:
|
|
||||||
return self.dev is not None or self.pre is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_postrelease(self) -> bool:
|
|
||||||
return self.post is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_devrelease(self) -> bool:
|
|
||||||
return self.dev is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def major(self) -> int:
|
|
||||||
return self.release[0] if len(self.release) >= 1 else 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def minor(self) -> int:
|
|
||||||
return self.release[1] if len(self.release) >= 2 else 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def micro(self) -> int:
|
|
||||||
return self.release[2] if len(self.release) >= 3 else 0
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_letter_version(
|
|
||||||
letter: str, number: Union[str, bytes, SupportsInt]
|
|
||||||
) -> 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.
|
|
||||||
if number is None:
|
|
||||||
number = 0
|
|
||||||
|
|
||||||
# We normalize any letters to their lower case form
|
|
||||||
letter = letter.lower()
|
|
||||||
|
|
||||||
# We consider some words to be alternate spellings of other words and
|
|
||||||
# in those cases we want to normalize the spellings to our preferred
|
|
||||||
# spelling.
|
|
||||||
if letter == "alpha":
|
|
||||||
letter = "a"
|
|
||||||
elif letter == "beta":
|
|
||||||
letter = "b"
|
|
||||||
elif letter in ["c", "pre", "preview"]:
|
|
||||||
letter = "rc"
|
|
||||||
elif letter in ["rev", "r"]:
|
|
||||||
letter = "post"
|
|
||||||
|
|
||||||
return letter, int(number)
|
|
||||||
if not letter and number:
|
|
||||||
# We assume if we are given a number, but we are not given a letter
|
|
||||||
# then this is using the implicit post release syntax (e.g. 1.0-1)
|
|
||||||
letter = "post"
|
|
||||||
|
|
||||||
return letter, int(number)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
_local_version_separators = re.compile(r"[\._-]")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_local_version(local: str) -> Optional[LocalType]:
|
|
||||||
"""
|
|
||||||
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
|
|
||||||
"""
|
|
||||||
if local is not None:
|
|
||||||
return tuple(
|
|
||||||
part.lower() if not part.isdigit() else int(part)
|
|
||||||
for part in _local_version_separators.split(local)
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _cmpkey(
|
|
||||||
epoch: int,
|
|
||||||
release: Tuple[int, ...],
|
|
||||||
pre: Optional[Tuple[str, int]],
|
|
||||||
post: Optional[Tuple[str, int]],
|
|
||||||
dev: Optional[Tuple[str, int]],
|
|
||||||
local: Optional[Tuple[SubLocalType]],
|
|
||||||
) -> 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
|
|
||||||
# re-reverse it back into the correct order and make it a tuple and use
|
|
||||||
# that for our sorting key.
|
|
||||||
_release = tuple(
|
|
||||||
reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
|
|
||||||
)
|
|
||||||
|
|
||||||
# We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
|
|
||||||
# We'll do this by abusing the pre segment, but we _only_ want to do this
|
|
||||||
# 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
|
|
||||||
# Versions without a pre-release (except as noted above) should sort after
|
|
||||||
# those with one.
|
|
||||||
elif pre is None:
|
|
||||||
_pre = Infinity
|
|
||||||
else:
|
|
||||||
_pre = pre
|
|
||||||
|
|
||||||
# Versions without a post segment should sort before those with one.
|
|
||||||
if post is None:
|
|
||||||
_post: PrePostDevType = NegativeInfinity
|
|
||||||
|
|
||||||
else:
|
|
||||||
_post = post
|
|
||||||
|
|
||||||
# Versions without a development segment should sort after those with one.
|
|
||||||
if dev is None:
|
|
||||||
_dev: PrePostDevType = Infinity
|
|
||||||
|
|
||||||
else:
|
|
||||||
_dev = dev
|
|
||||||
|
|
||||||
if local is None:
|
|
||||||
# Versions without a local segment should sort before those with one.
|
|
||||||
_local: LocalType = NegativeInfinity
|
|
||||||
else:
|
|
||||||
# Versions with a local segment need that segment parsed to implement
|
|
||||||
# the sorting rules in PEP440.
|
|
||||||
# - Alpha numeric segments sort before numeric segments
|
|
||||||
# - Alpha numeric segments sort lexicographically
|
|
||||||
# - Numeric segments sort numerically
|
|
||||||
# - Shorter versions sort before longer versions when the prefixes
|
|
||||||
# match exactly
|
|
||||||
_local = tuple(
|
|
||||||
(i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
|
|
||||||
)
|
|
||||||
|
|
||||||
return epoch, _release, _pre, _post, _dev, _local
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
# module pyparsing.py
|
|
||||||
#
|
|
||||||
# Copyright (c) 2003-2022 Paul T. McGuire
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
# a copy of this software and associated documentation files (the
|
|
||||||
# "Software"), to deal in the Software without restriction, including
|
|
||||||
# without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
# permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
# the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be
|
|
||||||
# included in all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
||||||
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
||||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
__doc__ = """
|
|
||||||
pyparsing module - Classes and methods to define and execute parsing grammars
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
The pyparsing module is an alternative approach to creating and
|
|
||||||
executing simple grammars, vs. the traditional lex/yacc approach, or the
|
|
||||||
use of regular expressions. With pyparsing, you don't need to learn
|
|
||||||
a new syntax for defining grammars or matching expressions - the parsing
|
|
||||||
module provides a library of classes that you use to construct the
|
|
||||||
grammar directly in Python.
|
|
||||||
|
|
||||||
Here is a program to parse "Hello, World!" (or any greeting of the form
|
|
||||||
``"<salutation>, <addressee>!"``), built up using :class:`Word`,
|
|
||||||
:class:`Literal`, and :class:`And` elements
|
|
||||||
(the :meth:`'+'<ParserElement.__add__>` operators create :class:`And` expressions,
|
|
||||||
and the strings are auto-converted to :class:`Literal` expressions)::
|
|
||||||
|
|
||||||
from pyparsing import Word, alphas
|
|
||||||
|
|
||||||
# define grammar of a greeting
|
|
||||||
greet = Word(alphas) + "," + Word(alphas) + "!"
|
|
||||||
|
|
||||||
hello = "Hello, World!"
|
|
||||||
print(hello, "->", greet.parse_string(hello))
|
|
||||||
|
|
||||||
The program outputs the following::
|
|
||||||
|
|
||||||
Hello, World! -> ['Hello', ',', 'World', '!']
|
|
||||||
|
|
||||||
The Python representation of the grammar is quite readable, owing to the
|
|
||||||
self-explanatory class names, and the use of :class:`'+'<And>`,
|
|
||||||
:class:`'|'<MatchFirst>`, :class:`'^'<Or>` and :class:`'&'<Each>` operators.
|
|
||||||
|
|
||||||
The :class:`ParseResults` object returned from
|
|
||||||
:class:`ParserElement.parseString` can be
|
|
||||||
accessed as a nested list, a dictionary, or an object with named
|
|
||||||
attributes.
|
|
||||||
|
|
||||||
The pyparsing module handles some of the problems that are typically
|
|
||||||
vexing when writing text parsers:
|
|
||||||
|
|
||||||
- extra or missing whitespace (the above program will also handle
|
|
||||||
"Hello,World!", "Hello , World !", etc.)
|
|
||||||
- quoted strings
|
|
||||||
- embedded comments
|
|
||||||
|
|
||||||
|
|
||||||
Getting Started -
|
|
||||||
-----------------
|
|
||||||
Visit the classes :class:`ParserElement` and :class:`ParseResults` to
|
|
||||||
see the base classes that most other pyparsing
|
|
||||||
classes inherit from. Use the docstrings for examples of how to:
|
|
||||||
|
|
||||||
- construct literal match expressions from :class:`Literal` and
|
|
||||||
:class:`CaselessLiteral` classes
|
|
||||||
- construct character word-group expressions using the :class:`Word`
|
|
||||||
class
|
|
||||||
- see how to create repetitive expressions using :class:`ZeroOrMore`
|
|
||||||
and :class:`OneOrMore` classes
|
|
||||||
- use :class:`'+'<And>`, :class:`'|'<MatchFirst>`, :class:`'^'<Or>`,
|
|
||||||
and :class:`'&'<Each>` operators to combine simple expressions into
|
|
||||||
more complex ones
|
|
||||||
- associate names with your parsed results using
|
|
||||||
:class:`ParserElement.setResultsName`
|
|
||||||
- access the parsed data, which is returned as a :class:`ParseResults`
|
|
||||||
object
|
|
||||||
- find some helpful expression short-cuts like :class:`delimitedList`
|
|
||||||
and :class:`oneOf`
|
|
||||||
- find more useful common expressions in the :class:`pyparsing_common`
|
|
||||||
namespace class
|
|
||||||
"""
|
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
|
|
||||||
class version_info(NamedTuple):
|
|
||||||
major: int
|
|
||||||
minor: int
|
|
||||||
micro: int
|
|
||||||
releaselevel: str
|
|
||||||
serial: int
|
|
||||||
|
|
||||||
@property
|
|
||||||
def __version__(self):
|
|
||||||
return (
|
|
||||||
"{}.{}.{}".format(self.major, self.minor, self.micro)
|
|
||||||
+ (
|
|
||||||
"{}{}{}".format(
|
|
||||||
"r" if self.releaselevel[0] == "c" else "",
|
|
||||||
self.releaselevel[0],
|
|
||||||
self.serial,
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
)[self.releaselevel == "final"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{} {} / {}".format(__name__, self.__version__, __version_time__)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "{}.{}({})".format(
|
|
||||||
__name__,
|
|
||||||
type(self).__name__,
|
|
||||||
", ".join("{}={!r}".format(*nv) for nv in zip(self._fields, self)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
__version_info__ = version_info(3, 0, 9, "final", 0)
|
|
||||||
__version_time__ = "05 May 2022 07:02 UTC"
|
|
||||||
__version__ = __version_info__.__version__
|
|
||||||
__versionTime__ = __version_time__
|
|
||||||
__author__ = "Paul McGuire <ptmcg.gm+pyparsing@gmail.com>"
|
|
||||||
|
|
||||||
from .util import *
|
|
||||||
from .exceptions import *
|
|
||||||
from .actions import *
|
|
||||||
from .core import __diag__, __compat__
|
|
||||||
from .results import *
|
|
||||||
from .core import *
|
|
||||||
from .core import _builtin_exprs as core_builtin_exprs
|
|
||||||
from .helpers import *
|
|
||||||
from .helpers import _builtin_exprs as helper_builtin_exprs
|
|
||||||
|
|
||||||
from .unicode import unicode_set, UnicodeRangeList, pyparsing_unicode as unicode
|
|
||||||
from .testing import pyparsing_test as testing
|
|
||||||
from .common import (
|
|
||||||
pyparsing_common as common,
|
|
||||||
_builtin_exprs as common_builtin_exprs,
|
|
||||||
)
|
|
||||||
|
|
||||||
# define backward compat synonyms
|
|
||||||
if "pyparsing_unicode" not in globals():
|
|
||||||
pyparsing_unicode = unicode
|
|
||||||
if "pyparsing_common" not in globals():
|
|
||||||
pyparsing_common = common
|
|
||||||
if "pyparsing_test" not in globals():
|
|
||||||
pyparsing_test = testing
|
|
||||||
|
|
||||||
core_builtin_exprs += common_builtin_exprs + helper_builtin_exprs
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"__version__",
|
|
||||||
"__version_time__",
|
|
||||||
"__author__",
|
|
||||||
"__compat__",
|
|
||||||
"__diag__",
|
|
||||||
"And",
|
|
||||||
"AtLineStart",
|
|
||||||
"AtStringStart",
|
|
||||||
"CaselessKeyword",
|
|
||||||
"CaselessLiteral",
|
|
||||||
"CharsNotIn",
|
|
||||||
"Combine",
|
|
||||||
"Dict",
|
|
||||||
"Each",
|
|
||||||
"Empty",
|
|
||||||
"FollowedBy",
|
|
||||||
"Forward",
|
|
||||||
"GoToColumn",
|
|
||||||
"Group",
|
|
||||||
"IndentedBlock",
|
|
||||||
"Keyword",
|
|
||||||
"LineEnd",
|
|
||||||
"LineStart",
|
|
||||||
"Literal",
|
|
||||||
"Located",
|
|
||||||
"PrecededBy",
|
|
||||||
"MatchFirst",
|
|
||||||
"NoMatch",
|
|
||||||
"NotAny",
|
|
||||||
"OneOrMore",
|
|
||||||
"OnlyOnce",
|
|
||||||
"OpAssoc",
|
|
||||||
"Opt",
|
|
||||||
"Optional",
|
|
||||||
"Or",
|
|
||||||
"ParseBaseException",
|
|
||||||
"ParseElementEnhance",
|
|
||||||
"ParseException",
|
|
||||||
"ParseExpression",
|
|
||||||
"ParseFatalException",
|
|
||||||
"ParseResults",
|
|
||||||
"ParseSyntaxException",
|
|
||||||
"ParserElement",
|
|
||||||
"PositionToken",
|
|
||||||
"QuotedString",
|
|
||||||
"RecursiveGrammarException",
|
|
||||||
"Regex",
|
|
||||||
"SkipTo",
|
|
||||||
"StringEnd",
|
|
||||||
"StringStart",
|
|
||||||
"Suppress",
|
|
||||||
"Token",
|
|
||||||
"TokenConverter",
|
|
||||||
"White",
|
|
||||||
"Word",
|
|
||||||
"WordEnd",
|
|
||||||
"WordStart",
|
|
||||||
"ZeroOrMore",
|
|
||||||
"Char",
|
|
||||||
"alphanums",
|
|
||||||
"alphas",
|
|
||||||
"alphas8bit",
|
|
||||||
"any_close_tag",
|
|
||||||
"any_open_tag",
|
|
||||||
"c_style_comment",
|
|
||||||
"col",
|
|
||||||
"common_html_entity",
|
|
||||||
"counted_array",
|
|
||||||
"cpp_style_comment",
|
|
||||||
"dbl_quoted_string",
|
|
||||||
"dbl_slash_comment",
|
|
||||||
"delimited_list",
|
|
||||||
"dict_of",
|
|
||||||
"empty",
|
|
||||||
"hexnums",
|
|
||||||
"html_comment",
|
|
||||||
"identchars",
|
|
||||||
"identbodychars",
|
|
||||||
"java_style_comment",
|
|
||||||
"line",
|
|
||||||
"line_end",
|
|
||||||
"line_start",
|
|
||||||
"lineno",
|
|
||||||
"make_html_tags",
|
|
||||||
"make_xml_tags",
|
|
||||||
"match_only_at_col",
|
|
||||||
"match_previous_expr",
|
|
||||||
"match_previous_literal",
|
|
||||||
"nested_expr",
|
|
||||||
"null_debug_action",
|
|
||||||
"nums",
|
|
||||||
"one_of",
|
|
||||||
"printables",
|
|
||||||
"punc8bit",
|
|
||||||
"python_style_comment",
|
|
||||||
"quoted_string",
|
|
||||||
"remove_quotes",
|
|
||||||
"replace_with",
|
|
||||||
"replace_html_entity",
|
|
||||||
"rest_of_line",
|
|
||||||
"sgl_quoted_string",
|
|
||||||
"srange",
|
|
||||||
"string_end",
|
|
||||||
"string_start",
|
|
||||||
"trace_parse_action",
|
|
||||||
"unicode_string",
|
|
||||||
"with_attribute",
|
|
||||||
"indentedBlock",
|
|
||||||
"original_text_for",
|
|
||||||
"ungroup",
|
|
||||||
"infix_notation",
|
|
||||||
"locatedExpr",
|
|
||||||
"with_class",
|
|
||||||
"CloseMatch",
|
|
||||||
"token_map",
|
|
||||||
"pyparsing_common",
|
|
||||||
"pyparsing_unicode",
|
|
||||||
"unicode_set",
|
|
||||||
"condition_as_parse_action",
|
|
||||||
"pyparsing_test",
|
|
||||||
# pre-PEP8 compatibility names
|
|
||||||
"__versionTime__",
|
|
||||||
"anyCloseTag",
|
|
||||||
"anyOpenTag",
|
|
||||||
"cStyleComment",
|
|
||||||
"commonHTMLEntity",
|
|
||||||
"countedArray",
|
|
||||||
"cppStyleComment",
|
|
||||||
"dblQuotedString",
|
|
||||||
"dblSlashComment",
|
|
||||||
"delimitedList",
|
|
||||||
"dictOf",
|
|
||||||
"htmlComment",
|
|
||||||
"javaStyleComment",
|
|
||||||
"lineEnd",
|
|
||||||
"lineStart",
|
|
||||||
"makeHTMLTags",
|
|
||||||
"makeXMLTags",
|
|
||||||
"matchOnlyAtCol",
|
|
||||||
"matchPreviousExpr",
|
|
||||||
"matchPreviousLiteral",
|
|
||||||
"nestedExpr",
|
|
||||||
"nullDebugAction",
|
|
||||||
"oneOf",
|
|
||||||
"opAssoc",
|
|
||||||
"pythonStyleComment",
|
|
||||||
"quotedString",
|
|
||||||
"removeQuotes",
|
|
||||||
"replaceHTMLEntity",
|
|
||||||
"replaceWith",
|
|
||||||
"restOfLine",
|
|
||||||
"sglQuotedString",
|
|
||||||
"stringEnd",
|
|
||||||
"stringStart",
|
|
||||||
"traceParseAction",
|
|
||||||
"unicodeString",
|
|
||||||
"withAttribute",
|
|
||||||
"indentedBlock",
|
|
||||||
"originalTextFor",
|
|
||||||
"infixNotation",
|
|
||||||
"locatedExpr",
|
|
||||||
"withClass",
|
|
||||||
"tokenMap",
|
|
||||||
"conditionAsParseAction",
|
|
||||||
"autoname_elements",
|
|
||||||
]
|
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,207 +0,0 @@
|
|||||||
# actions.py
|
|
||||||
|
|
||||||
from .exceptions import ParseException
|
|
||||||
from .util import col
|
|
||||||
|
|
||||||
|
|
||||||
class OnlyOnce:
|
|
||||||
"""
|
|
||||||
Wrapper for parse actions, to ensure they are only called once.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, method_call):
|
|
||||||
from .core import _trim_arity
|
|
||||||
|
|
||||||
self.callable = _trim_arity(method_call)
|
|
||||||
self.called = False
|
|
||||||
|
|
||||||
def __call__(self, s, l, t):
|
|
||||||
if not self.called:
|
|
||||||
results = self.callable(s, l, t)
|
|
||||||
self.called = True
|
|
||||||
return results
|
|
||||||
raise ParseException(s, l, "OnlyOnce obj called multiple times w/out reset")
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""
|
|
||||||
Allow the associated parse action to be called once more.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.called = False
|
|
||||||
|
|
||||||
|
|
||||||
def match_only_at_col(n):
|
|
||||||
"""
|
|
||||||
Helper method for defining parse actions that require matching at
|
|
||||||
a specific column in the input text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def verify_col(strg, locn, toks):
|
|
||||||
if col(locn, strg) != n:
|
|
||||||
raise ParseException(strg, locn, "matched token not at column {}".format(n))
|
|
||||||
|
|
||||||
return verify_col
|
|
||||||
|
|
||||||
|
|
||||||
def replace_with(repl_str):
|
|
||||||
"""
|
|
||||||
Helper method for common parse actions that simply return
|
|
||||||
a literal value. Especially useful when used with
|
|
||||||
:class:`transform_string<ParserElement.transform_string>` ().
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
num = Word(nums).set_parse_action(lambda toks: int(toks[0]))
|
|
||||||
na = one_of("N/A NA").set_parse_action(replace_with(math.nan))
|
|
||||||
term = na | num
|
|
||||||
|
|
||||||
term[1, ...].parse_string("324 234 N/A 234") # -> [324, 234, nan, 234]
|
|
||||||
"""
|
|
||||||
return lambda s, l, t: [repl_str]
|
|
||||||
|
|
||||||
|
|
||||||
def remove_quotes(s, l, t):
|
|
||||||
"""
|
|
||||||
Helper parse action for removing quotation marks from parsed
|
|
||||||
quoted strings.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
# by default, quotation marks are included in parsed results
|
|
||||||
quoted_string.parse_string("'Now is the Winter of our Discontent'") # -> ["'Now is the Winter of our Discontent'"]
|
|
||||||
|
|
||||||
# use remove_quotes to strip quotation marks from parsed results
|
|
||||||
quoted_string.set_parse_action(remove_quotes)
|
|
||||||
quoted_string.parse_string("'Now is the Winter of our Discontent'") # -> ["Now is the Winter of our Discontent"]
|
|
||||||
"""
|
|
||||||
return t[0][1:-1]
|
|
||||||
|
|
||||||
|
|
||||||
def with_attribute(*args, **attr_dict):
|
|
||||||
"""
|
|
||||||
Helper to create a validating parse action to be used with start
|
|
||||||
tags created with :class:`make_xml_tags` or
|
|
||||||
:class:`make_html_tags`. Use ``with_attribute`` to qualify
|
|
||||||
a starting tag with a required attribute value, to avoid false
|
|
||||||
matches on common tags such as ``<TD>`` or ``<DIV>``.
|
|
||||||
|
|
||||||
Call ``with_attribute`` with a series of attribute names and
|
|
||||||
values. Specify the list of filter attributes names and values as:
|
|
||||||
|
|
||||||
- keyword arguments, as in ``(align="right")``, or
|
|
||||||
- as an explicit dict with ``**`` operator, when an attribute
|
|
||||||
name is also a Python reserved word, as in ``**{"class":"Customer", "align":"right"}``
|
|
||||||
- a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align", "right"))``
|
|
||||||
|
|
||||||
For attribute names with a namespace prefix, you must use the second
|
|
||||||
form. Attribute names are matched insensitive to upper/lower case.
|
|
||||||
|
|
||||||
If just testing for ``class`` (with or without a namespace), use
|
|
||||||
:class:`with_class`.
|
|
||||||
|
|
||||||
To verify that the attribute exists, but without specifying a value,
|
|
||||||
pass ``with_attribute.ANY_VALUE`` as the value.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
html = '''
|
|
||||||
<div>
|
|
||||||
Some text
|
|
||||||
<div type="grid">1 4 0 1 0</div>
|
|
||||||
<div type="graph">1,3 2,3 1,1</div>
|
|
||||||
<div>this has no type</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
'''
|
|
||||||
div,div_end = make_html_tags("div")
|
|
||||||
|
|
||||||
# only match div tag having a type attribute with value "grid"
|
|
||||||
div_grid = div().set_parse_action(with_attribute(type="grid"))
|
|
||||||
grid_expr = div_grid + SkipTo(div | div_end)("body")
|
|
||||||
for grid_header in grid_expr.search_string(html):
|
|
||||||
print(grid_header.body)
|
|
||||||
|
|
||||||
# construct a match with any div tag having a type attribute, regardless of the value
|
|
||||||
div_any_type = div().set_parse_action(with_attribute(type=with_attribute.ANY_VALUE))
|
|
||||||
div_expr = div_any_type + SkipTo(div | div_end)("body")
|
|
||||||
for div_header in div_expr.search_string(html):
|
|
||||||
print(div_header.body)
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
1 4 0 1 0
|
|
||||||
|
|
||||||
1 4 0 1 0
|
|
||||||
1,3 2,3 1,1
|
|
||||||
"""
|
|
||||||
if args:
|
|
||||||
attrs = args[:]
|
|
||||||
else:
|
|
||||||
attrs = attr_dict.items()
|
|
||||||
attrs = [(k, v) for k, v in attrs]
|
|
||||||
|
|
||||||
def pa(s, l, tokens):
|
|
||||||
for attrName, attrValue in attrs:
|
|
||||||
if attrName not in tokens:
|
|
||||||
raise ParseException(s, l, "no matching attribute " + attrName)
|
|
||||||
if attrValue != with_attribute.ANY_VALUE and tokens[attrName] != attrValue:
|
|
||||||
raise ParseException(
|
|
||||||
s,
|
|
||||||
l,
|
|
||||||
"attribute {!r} has value {!r}, must be {!r}".format(
|
|
||||||
attrName, tokens[attrName], attrValue
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return pa
|
|
||||||
|
|
||||||
|
|
||||||
with_attribute.ANY_VALUE = object()
|
|
||||||
|
|
||||||
|
|
||||||
def with_class(classname, namespace=""):
|
|
||||||
"""
|
|
||||||
Simplified version of :class:`with_attribute` when
|
|
||||||
matching on a div class - made difficult because ``class`` is
|
|
||||||
a reserved word in Python.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
html = '''
|
|
||||||
<div>
|
|
||||||
Some text
|
|
||||||
<div class="grid">1 4 0 1 0</div>
|
|
||||||
<div class="graph">1,3 2,3 1,1</div>
|
|
||||||
<div>this <div> has no class</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
'''
|
|
||||||
div,div_end = make_html_tags("div")
|
|
||||||
div_grid = div().set_parse_action(with_class("grid"))
|
|
||||||
|
|
||||||
grid_expr = div_grid + SkipTo(div | div_end)("body")
|
|
||||||
for grid_header in grid_expr.search_string(html):
|
|
||||||
print(grid_header.body)
|
|
||||||
|
|
||||||
div_any_type = div().set_parse_action(with_class(withAttribute.ANY_VALUE))
|
|
||||||
div_expr = div_any_type + SkipTo(div | div_end)("body")
|
|
||||||
for div_header in div_expr.search_string(html):
|
|
||||||
print(div_header.body)
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
1 4 0 1 0
|
|
||||||
|
|
||||||
1 4 0 1 0
|
|
||||||
1,3 2,3 1,1
|
|
||||||
"""
|
|
||||||
classattr = "{}:class".format(namespace) if namespace else "class"
|
|
||||||
return with_attribute(**{classattr: classname})
|
|
||||||
|
|
||||||
|
|
||||||
# pre-PEP8 compatibility symbols
|
|
||||||
replaceWith = replace_with
|
|
||||||
removeQuotes = remove_quotes
|
|
||||||
withAttribute = with_attribute
|
|
||||||
withClass = with_class
|
|
||||||
matchOnlyAtCol = match_only_at_col
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
# common.py
|
|
||||||
from .core import *
|
|
||||||
from .helpers import delimited_list, any_open_tag, any_close_tag
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
# some other useful expressions - using lower-case class name since we are really using this as a namespace
|
|
||||||
class pyparsing_common:
|
|
||||||
"""Here are some common low-level expressions that may be useful in
|
|
||||||
jump-starting parser development:
|
|
||||||
|
|
||||||
- numeric forms (:class:`integers<integer>`, :class:`reals<real>`,
|
|
||||||
:class:`scientific notation<sci_real>`)
|
|
||||||
- common :class:`programming identifiers<identifier>`
|
|
||||||
- network addresses (:class:`MAC<mac_address>`,
|
|
||||||
:class:`IPv4<ipv4_address>`, :class:`IPv6<ipv6_address>`)
|
|
||||||
- ISO8601 :class:`dates<iso8601_date>` and
|
|
||||||
:class:`datetime<iso8601_datetime>`
|
|
||||||
- :class:`UUID<uuid>`
|
|
||||||
- :class:`comma-separated list<comma_separated_list>`
|
|
||||||
- :class:`url`
|
|
||||||
|
|
||||||
Parse actions:
|
|
||||||
|
|
||||||
- :class:`convertToInteger`
|
|
||||||
- :class:`convertToFloat`
|
|
||||||
- :class:`convertToDate`
|
|
||||||
- :class:`convertToDatetime`
|
|
||||||
- :class:`stripHTMLTags`
|
|
||||||
- :class:`upcaseTokens`
|
|
||||||
- :class:`downcaseTokens`
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
pyparsing_common.number.runTests('''
|
|
||||||
# any int or real number, returned as the appropriate type
|
|
||||||
100
|
|
||||||
-100
|
|
||||||
+100
|
|
||||||
3.14159
|
|
||||||
6.02e23
|
|
||||||
1e-12
|
|
||||||
''')
|
|
||||||
|
|
||||||
pyparsing_common.fnumber.runTests('''
|
|
||||||
# any int or real number, returned as float
|
|
||||||
100
|
|
||||||
-100
|
|
||||||
+100
|
|
||||||
3.14159
|
|
||||||
6.02e23
|
|
||||||
1e-12
|
|
||||||
''')
|
|
||||||
|
|
||||||
pyparsing_common.hex_integer.runTests('''
|
|
||||||
# hex numbers
|
|
||||||
100
|
|
||||||
FF
|
|
||||||
''')
|
|
||||||
|
|
||||||
pyparsing_common.fraction.runTests('''
|
|
||||||
# fractions
|
|
||||||
1/2
|
|
||||||
-3/4
|
|
||||||
''')
|
|
||||||
|
|
||||||
pyparsing_common.mixed_integer.runTests('''
|
|
||||||
# mixed fractions
|
|
||||||
1
|
|
||||||
1/2
|
|
||||||
-3/4
|
|
||||||
1-3/4
|
|
||||||
''')
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID))
|
|
||||||
pyparsing_common.uuid.runTests('''
|
|
||||||
# uuid
|
|
||||||
12345678-1234-5678-1234-567812345678
|
|
||||||
''')
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
# any int or real number, returned as the appropriate type
|
|
||||||
100
|
|
||||||
[100]
|
|
||||||
|
|
||||||
-100
|
|
||||||
[-100]
|
|
||||||
|
|
||||||
+100
|
|
||||||
[100]
|
|
||||||
|
|
||||||
3.14159
|
|
||||||
[3.14159]
|
|
||||||
|
|
||||||
6.02e23
|
|
||||||
[6.02e+23]
|
|
||||||
|
|
||||||
1e-12
|
|
||||||
[1e-12]
|
|
||||||
|
|
||||||
# any int or real number, returned as float
|
|
||||||
100
|
|
||||||
[100.0]
|
|
||||||
|
|
||||||
-100
|
|
||||||
[-100.0]
|
|
||||||
|
|
||||||
+100
|
|
||||||
[100.0]
|
|
||||||
|
|
||||||
3.14159
|
|
||||||
[3.14159]
|
|
||||||
|
|
||||||
6.02e23
|
|
||||||
[6.02e+23]
|
|
||||||
|
|
||||||
1e-12
|
|
||||||
[1e-12]
|
|
||||||
|
|
||||||
# hex numbers
|
|
||||||
100
|
|
||||||
[256]
|
|
||||||
|
|
||||||
FF
|
|
||||||
[255]
|
|
||||||
|
|
||||||
# fractions
|
|
||||||
1/2
|
|
||||||
[0.5]
|
|
||||||
|
|
||||||
-3/4
|
|
||||||
[-0.75]
|
|
||||||
|
|
||||||
# mixed fractions
|
|
||||||
1
|
|
||||||
[1]
|
|
||||||
|
|
||||||
1/2
|
|
||||||
[0.5]
|
|
||||||
|
|
||||||
-3/4
|
|
||||||
[-0.75]
|
|
||||||
|
|
||||||
1-3/4
|
|
||||||
[1.75]
|
|
||||||
|
|
||||||
# uuid
|
|
||||||
12345678-1234-5678-1234-567812345678
|
|
||||||
[UUID('12345678-1234-5678-1234-567812345678')]
|
|
||||||
"""
|
|
||||||
|
|
||||||
convert_to_integer = token_map(int)
|
|
||||||
"""
|
|
||||||
Parse action for converting parsed integers to Python int
|
|
||||||
"""
|
|
||||||
|
|
||||||
convert_to_float = token_map(float)
|
|
||||||
"""
|
|
||||||
Parse action for converting parsed numbers to Python float
|
|
||||||
"""
|
|
||||||
|
|
||||||
integer = Word(nums).set_name("integer").set_parse_action(convert_to_integer)
|
|
||||||
"""expression that parses an unsigned integer, returns an int"""
|
|
||||||
|
|
||||||
hex_integer = (
|
|
||||||
Word(hexnums).set_name("hex integer").set_parse_action(token_map(int, 16))
|
|
||||||
)
|
|
||||||
"""expression that parses a hexadecimal integer, returns an int"""
|
|
||||||
|
|
||||||
signed_integer = (
|
|
||||||
Regex(r"[+-]?\d+")
|
|
||||||
.set_name("signed integer")
|
|
||||||
.set_parse_action(convert_to_integer)
|
|
||||||
)
|
|
||||||
"""expression that parses an integer with optional leading sign, returns an int"""
|
|
||||||
|
|
||||||
fraction = (
|
|
||||||
signed_integer().set_parse_action(convert_to_float)
|
|
||||||
+ "/"
|
|
||||||
+ signed_integer().set_parse_action(convert_to_float)
|
|
||||||
).set_name("fraction")
|
|
||||||
"""fractional expression of an integer divided by an integer, returns a float"""
|
|
||||||
fraction.add_parse_action(lambda tt: tt[0] / tt[-1])
|
|
||||||
|
|
||||||
mixed_integer = (
|
|
||||||
fraction | signed_integer + Opt(Opt("-").suppress() + fraction)
|
|
||||||
).set_name("fraction or mixed integer-fraction")
|
|
||||||
"""mixed integer of the form 'integer - fraction', with optional leading integer, returns float"""
|
|
||||||
mixed_integer.add_parse_action(sum)
|
|
||||||
|
|
||||||
real = (
|
|
||||||
Regex(r"[+-]?(?:\d+\.\d*|\.\d+)")
|
|
||||||
.set_name("real number")
|
|
||||||
.set_parse_action(convert_to_float)
|
|
||||||
)
|
|
||||||
"""expression that parses a floating point number and returns a float"""
|
|
||||||
|
|
||||||
sci_real = (
|
|
||||||
Regex(r"[+-]?(?:\d+(?:[eE][+-]?\d+)|(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?)")
|
|
||||||
.set_name("real number with scientific notation")
|
|
||||||
.set_parse_action(convert_to_float)
|
|
||||||
)
|
|
||||||
"""expression that parses a floating point number with optional
|
|
||||||
scientific notation and returns a float"""
|
|
||||||
|
|
||||||
# streamlining this expression makes the docs nicer-looking
|
|
||||||
number = (sci_real | real | signed_integer).setName("number").streamline()
|
|
||||||
"""any numeric expression, returns the corresponding Python type"""
|
|
||||||
|
|
||||||
fnumber = (
|
|
||||||
Regex(r"[+-]?\d+\.?\d*([eE][+-]?\d+)?")
|
|
||||||
.set_name("fnumber")
|
|
||||||
.set_parse_action(convert_to_float)
|
|
||||||
)
|
|
||||||
"""any int or real number, returned as float"""
|
|
||||||
|
|
||||||
identifier = Word(identchars, identbodychars).set_name("identifier")
|
|
||||||
"""typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')"""
|
|
||||||
|
|
||||||
ipv4_address = Regex(
|
|
||||||
r"(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}"
|
|
||||||
).set_name("IPv4 address")
|
|
||||||
"IPv4 address (``0.0.0.0 - 255.255.255.255``)"
|
|
||||||
|
|
||||||
_ipv6_part = Regex(r"[0-9a-fA-F]{1,4}").set_name("hex_integer")
|
|
||||||
_full_ipv6_address = (_ipv6_part + (":" + _ipv6_part) * 7).set_name(
|
|
||||||
"full IPv6 address"
|
|
||||||
)
|
|
||||||
_short_ipv6_address = (
|
|
||||||
Opt(_ipv6_part + (":" + _ipv6_part) * (0, 6))
|
|
||||||
+ "::"
|
|
||||||
+ Opt(_ipv6_part + (":" + _ipv6_part) * (0, 6))
|
|
||||||
).set_name("short IPv6 address")
|
|
||||||
_short_ipv6_address.add_condition(
|
|
||||||
lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8
|
|
||||||
)
|
|
||||||
_mixed_ipv6_address = ("::ffff:" + ipv4_address).set_name("mixed IPv6 address")
|
|
||||||
ipv6_address = Combine(
|
|
||||||
(_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).set_name(
|
|
||||||
"IPv6 address"
|
|
||||||
)
|
|
||||||
).set_name("IPv6 address")
|
|
||||||
"IPv6 address (long, short, or mixed form)"
|
|
||||||
|
|
||||||
mac_address = Regex(
|
|
||||||
r"[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\1[0-9a-fA-F]{2}){4}"
|
|
||||||
).set_name("MAC address")
|
|
||||||
"MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_to_date(fmt: str = "%Y-%m-%d"):
|
|
||||||
"""
|
|
||||||
Helper to create a parse action for converting parsed date string to Python datetime.date
|
|
||||||
|
|
||||||
Params -
|
|
||||||
- fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%d"``)
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
date_expr = pyparsing_common.iso8601_date.copy()
|
|
||||||
date_expr.setParseAction(pyparsing_common.convertToDate())
|
|
||||||
print(date_expr.parseString("1999-12-31"))
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
[datetime.date(1999, 12, 31)]
|
|
||||||
"""
|
|
||||||
|
|
||||||
def cvt_fn(ss, ll, tt):
|
|
||||||
try:
|
|
||||||
return datetime.strptime(tt[0], fmt).date()
|
|
||||||
except ValueError as ve:
|
|
||||||
raise ParseException(ss, ll, str(ve))
|
|
||||||
|
|
||||||
return cvt_fn
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def convert_to_datetime(fmt: str = "%Y-%m-%dT%H:%M:%S.%f"):
|
|
||||||
"""Helper to create a parse action for converting parsed
|
|
||||||
datetime string to Python datetime.datetime
|
|
||||||
|
|
||||||
Params -
|
|
||||||
- fmt - format to be passed to datetime.strptime (default= ``"%Y-%m-%dT%H:%M:%S.%f"``)
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
dt_expr = pyparsing_common.iso8601_datetime.copy()
|
|
||||||
dt_expr.setParseAction(pyparsing_common.convertToDatetime())
|
|
||||||
print(dt_expr.parseString("1999-12-31T23:59:59.999"))
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
[datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)]
|
|
||||||
"""
|
|
||||||
|
|
||||||
def cvt_fn(s, l, t):
|
|
||||||
try:
|
|
||||||
return datetime.strptime(t[0], fmt)
|
|
||||||
except ValueError as ve:
|
|
||||||
raise ParseException(s, l, str(ve))
|
|
||||||
|
|
||||||
return cvt_fn
|
|
||||||
|
|
||||||
iso8601_date = Regex(
|
|
||||||
r"(?P<year>\d{4})(?:-(?P<month>\d\d)(?:-(?P<day>\d\d))?)?"
|
|
||||||
).set_name("ISO8601 date")
|
|
||||||
"ISO8601 date (``yyyy-mm-dd``)"
|
|
||||||
|
|
||||||
iso8601_datetime = Regex(
|
|
||||||
r"(?P<year>\d{4})-(?P<month>\d\d)-(?P<day>\d\d)[T ](?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d(\.\d*)?)?)?(?P<tz>Z|[+-]\d\d:?\d\d)?"
|
|
||||||
).set_name("ISO8601 datetime")
|
|
||||||
"ISO8601 datetime (``yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)``) - trailing seconds, milliseconds, and timezone optional; accepts separating ``'T'`` or ``' '``"
|
|
||||||
|
|
||||||
uuid = Regex(r"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}").set_name("UUID")
|
|
||||||
"UUID (``xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx``)"
|
|
||||||
|
|
||||||
_html_stripper = any_open_tag.suppress() | any_close_tag.suppress()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def strip_html_tags(s: str, l: int, tokens: ParseResults):
|
|
||||||
"""Parse action to remove HTML tags from web page HTML source
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
# strip HTML links from normal text
|
|
||||||
text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>'
|
|
||||||
td, td_end = makeHTMLTags("TD")
|
|
||||||
table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end
|
|
||||||
print(table_text.parseString(text).body)
|
|
||||||
|
|
||||||
Prints::
|
|
||||||
|
|
||||||
More info at the pyparsing wiki page
|
|
||||||
"""
|
|
||||||
return pyparsing_common._html_stripper.transform_string(tokens[0])
|
|
||||||
|
|
||||||
_commasepitem = (
|
|
||||||
Combine(
|
|
||||||
OneOrMore(
|
|
||||||
~Literal(",")
|
|
||||||
+ ~LineEnd()
|
|
||||||
+ Word(printables, exclude_chars=",")
|
|
||||||
+ Opt(White(" \t") + ~FollowedBy(LineEnd() | ","))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.streamline()
|
|
||||||
.set_name("commaItem")
|
|
||||||
)
|
|
||||||
comma_separated_list = delimited_list(
|
|
||||||
Opt(quoted_string.copy() | _commasepitem, default="")
|
|
||||||
).set_name("comma separated list")
|
|
||||||
"""Predefined expression of 1 or more printable words or quoted strings, separated by commas."""
|
|
||||||
|
|
||||||
upcase_tokens = staticmethod(token_map(lambda t: t.upper()))
|
|
||||||
"""Parse action to convert tokens to upper case."""
|
|
||||||
|
|
||||||
downcase_tokens = staticmethod(token_map(lambda t: t.lower()))
|
|
||||||
"""Parse action to convert tokens to lower case."""
|
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
url = Regex(
|
|
||||||
# https://mathiasbynens.be/demo/url-regex
|
|
||||||
# https://gist.github.com/dperini/729294
|
|
||||||
r"^" +
|
|
||||||
# protocol identifier (optional)
|
|
||||||
# short syntax // still required
|
|
||||||
r"(?:(?:(?P<scheme>https?|ftp):)?\/\/)" +
|
|
||||||
# user:pass BasicAuth (optional)
|
|
||||||
r"(?:(?P<auth>\S+(?::\S*)?)@)?" +
|
|
||||||
r"(?P<host>" +
|
|
||||||
# IP address exclusion
|
|
||||||
# private & local networks
|
|
||||||
r"(?!(?:10|127)(?:\.\d{1,3}){3})" +
|
|
||||||
r"(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})" +
|
|
||||||
r"(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})" +
|
|
||||||
# IP address dotted notation octets
|
|
||||||
# excludes loopback network 0.0.0.0
|
|
||||||
# excludes reserved space >= 224.0.0.0
|
|
||||||
# excludes network & broadcast addresses
|
|
||||||
# (first & last IP address of each class)
|
|
||||||
r"(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])" +
|
|
||||||
r"(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}" +
|
|
||||||
r"(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))" +
|
|
||||||
r"|" +
|
|
||||||
# host & domain names, may end with dot
|
|
||||||
# can be replaced by a shortest alternative
|
|
||||||
# (?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.)+
|
|
||||||
r"(?:" +
|
|
||||||
r"(?:" +
|
|
||||||
r"[a-z0-9\u00a1-\uffff]" +
|
|
||||||
r"[a-z0-9\u00a1-\uffff_-]{0,62}" +
|
|
||||||
r")?" +
|
|
||||||
r"[a-z0-9\u00a1-\uffff]\." +
|
|
||||||
r")+" +
|
|
||||||
# TLD identifier name, may end with dot
|
|
||||||
r"(?:[a-z\u00a1-\uffff]{2,}\.?)" +
|
|
||||||
r")" +
|
|
||||||
# port number (optional)
|
|
||||||
r"(:(?P<port>\d{2,5}))?" +
|
|
||||||
# resource path (optional)
|
|
||||||
r"(?P<path>\/[^?# ]*)?" +
|
|
||||||
# query string (optional)
|
|
||||||
r"(\?(?P<query>[^#]*))?" +
|
|
||||||
# fragment (optional)
|
|
||||||
r"(#(?P<fragment>\S*))?" +
|
|
||||||
r"$"
|
|
||||||
).set_name("url")
|
|
||||||
# fmt: on
|
|
||||||
|
|
||||||
# pre-PEP8 compatibility names
|
|
||||||
convertToInteger = convert_to_integer
|
|
||||||
convertToFloat = convert_to_float
|
|
||||||
convertToDate = convert_to_date
|
|
||||||
convertToDatetime = convert_to_datetime
|
|
||||||
stripHTMLTags = strip_html_tags
|
|
||||||
upcaseTokens = upcase_tokens
|
|
||||||
downcaseTokens = downcase_tokens
|
|
||||||
|
|
||||||
|
|
||||||
_builtin_exprs = [
|
|
||||||
v for v in vars(pyparsing_common).values() if isinstance(v, ParserElement)
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,642 +0,0 @@
|
|||||||
import railroad
|
|
||||||
import pyparsing
|
|
||||||
import typing
|
|
||||||
from typing import (
|
|
||||||
List,
|
|
||||||
NamedTuple,
|
|
||||||
Generic,
|
|
||||||
TypeVar,
|
|
||||||
Dict,
|
|
||||||
Callable,
|
|
||||||
Set,
|
|
||||||
Iterable,
|
|
||||||
)
|
|
||||||
from jinja2 import Template
|
|
||||||
from io import StringIO
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
|
|
||||||
jinja2_template_source = """\
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
{% if not head %}
|
|
||||||
<style type="text/css">
|
|
||||||
.railroad-heading {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% else %}
|
|
||||||
{{ head | safe }}
|
|
||||||
{% endif %}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{ body | safe }}
|
|
||||||
{% for diagram in diagrams %}
|
|
||||||
<div class="railroad-group">
|
|
||||||
<h1 class="railroad-heading">{{ diagram.title }}</h1>
|
|
||||||
<div class="railroad-description">{{ diagram.text }}</div>
|
|
||||||
<div class="railroad-svg">
|
|
||||||
{{ diagram.svg }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(jinja2_template_source)
|
|
||||||
|
|
||||||
# Note: ideally this would be a dataclass, but we're supporting Python 3.5+ so we can't do this yet
|
|
||||||
NamedDiagram = NamedTuple(
|
|
||||||
"NamedDiagram",
|
|
||||||
[("name", str), ("diagram", typing.Optional[railroad.DiagramItem]), ("index", int)],
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
A simple structure for associating a name with a railroad diagram
|
|
||||||
"""
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
class EachItem(railroad.Group):
|
|
||||||
"""
|
|
||||||
Custom railroad item to compose a:
|
|
||||||
- Group containing a
|
|
||||||
- OneOrMore containing a
|
|
||||||
- Choice of the elements in the Each
|
|
||||||
with the group label indicating that all must be matched
|
|
||||||
"""
|
|
||||||
|
|
||||||
all_label = "[ALL]"
|
|
||||||
|
|
||||||
def __init__(self, *items):
|
|
||||||
choice_item = railroad.Choice(len(items) - 1, *items)
|
|
||||||
one_or_more_item = railroad.OneOrMore(item=choice_item)
|
|
||||||
super().__init__(one_or_more_item, label=self.all_label)
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotatedItem(railroad.Group):
|
|
||||||
"""
|
|
||||||
Simple subclass of Group that creates an annotation label
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, label: str, item):
|
|
||||||
super().__init__(item=item, label="[{}]".format(label) if label else label)
|
|
||||||
|
|
||||||
|
|
||||||
class EditablePartial(Generic[T]):
|
|
||||||
"""
|
|
||||||
Acts like a functools.partial, but can be edited. In other words, it represents a type that hasn't yet been
|
|
||||||
constructed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# We need this here because the railroad constructors actually transform the data, so can't be called until the
|
|
||||||
# entire tree is assembled
|
|
||||||
|
|
||||||
def __init__(self, func: Callable[..., T], args: list, kwargs: dict):
|
|
||||||
self.func = func
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_call(cls, func: Callable[..., T], *args, **kwargs) -> "EditablePartial[T]":
|
|
||||||
"""
|
|
||||||
If you call this function in the same way that you would call the constructor, it will store the arguments
|
|
||||||
as you expect. For example EditablePartial.from_call(Fraction, 1, 3)() == Fraction(1, 3)
|
|
||||||
"""
|
|
||||||
return EditablePartial(func=func, args=list(args), kwargs=kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return self.kwargs["name"]
|
|
||||||
|
|
||||||
def __call__(self) -> T:
|
|
||||||
"""
|
|
||||||
Evaluate the partial and return the result
|
|
||||||
"""
|
|
||||||
args = self.args.copy()
|
|
||||||
kwargs = self.kwargs.copy()
|
|
||||||
|
|
||||||
# This is a helpful hack to allow you to specify varargs parameters (e.g. *args) as keyword args (e.g.
|
|
||||||
# args=['list', 'of', 'things'])
|
|
||||||
arg_spec = inspect.getfullargspec(self.func)
|
|
||||||
if arg_spec.varargs in self.kwargs:
|
|
||||||
args += kwargs.pop(arg_spec.varargs)
|
|
||||||
|
|
||||||
return self.func(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def railroad_to_html(diagrams: List[NamedDiagram], **kwargs) -> str:
|
|
||||||
"""
|
|
||||||
Given a list of NamedDiagram, produce a single HTML string that visualises those diagrams
|
|
||||||
:params kwargs: kwargs to be passed in to the template
|
|
||||||
"""
|
|
||||||
data = []
|
|
||||||
for diagram in diagrams:
|
|
||||||
if diagram.diagram is None:
|
|
||||||
continue
|
|
||||||
io = StringIO()
|
|
||||||
diagram.diagram.writeSvg(io.write)
|
|
||||||
title = diagram.name
|
|
||||||
if diagram.index == 0:
|
|
||||||
title += " (root)"
|
|
||||||
data.append({"title": title, "text": "", "svg": io.getvalue()})
|
|
||||||
|
|
||||||
return template.render(diagrams=data, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_partial(partial: "EditablePartial[T]") -> T:
|
|
||||||
"""
|
|
||||||
Recursively resolves a collection of Partials into whatever type they are
|
|
||||||
"""
|
|
||||||
if isinstance(partial, EditablePartial):
|
|
||||||
partial.args = resolve_partial(partial.args)
|
|
||||||
partial.kwargs = resolve_partial(partial.kwargs)
|
|
||||||
return partial()
|
|
||||||
elif isinstance(partial, list):
|
|
||||||
return [resolve_partial(x) for x in partial]
|
|
||||||
elif isinstance(partial, dict):
|
|
||||||
return {key: resolve_partial(x) for key, x in partial.items()}
|
|
||||||
else:
|
|
||||||
return partial
|
|
||||||
|
|
||||||
|
|
||||||
def to_railroad(
|
|
||||||
element: pyparsing.ParserElement,
|
|
||||||
diagram_kwargs: typing.Optional[dict] = None,
|
|
||||||
vertical: int = 3,
|
|
||||||
show_results_names: bool = False,
|
|
||||||
show_groups: bool = False,
|
|
||||||
) -> List[NamedDiagram]:
|
|
||||||
"""
|
|
||||||
Convert a pyparsing element tree into a list of diagrams. This is the recommended entrypoint to diagram
|
|
||||||
creation if you want to access the Railroad tree before it is converted to HTML
|
|
||||||
:param element: base element of the parser being diagrammed
|
|
||||||
:param diagram_kwargs: kwargs to pass to the Diagram() constructor
|
|
||||||
:param vertical: (optional) - int - limit at which number of alternatives should be
|
|
||||||
shown vertically instead of horizontally
|
|
||||||
:param show_results_names - bool to indicate whether results name annotations should be
|
|
||||||
included in the diagram
|
|
||||||
:param show_groups - bool to indicate whether groups should be highlighted with an unlabeled
|
|
||||||
surrounding box
|
|
||||||
"""
|
|
||||||
# Convert the whole tree underneath the root
|
|
||||||
lookup = ConverterState(diagram_kwargs=diagram_kwargs or {})
|
|
||||||
_to_diagram_element(
|
|
||||||
element,
|
|
||||||
lookup=lookup,
|
|
||||||
parent=None,
|
|
||||||
vertical=vertical,
|
|
||||||
show_results_names=show_results_names,
|
|
||||||
show_groups=show_groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
root_id = id(element)
|
|
||||||
# Convert the root if it hasn't been already
|
|
||||||
if root_id in lookup:
|
|
||||||
if not element.customName:
|
|
||||||
lookup[root_id].name = ""
|
|
||||||
lookup[root_id].mark_for_extraction(root_id, lookup, force=True)
|
|
||||||
|
|
||||||
# Now that we're finished, we can convert from intermediate structures into Railroad elements
|
|
||||||
diags = list(lookup.diagrams.values())
|
|
||||||
if len(diags) > 1:
|
|
||||||
# collapse out duplicate diags with the same name
|
|
||||||
seen = set()
|
|
||||||
deduped_diags = []
|
|
||||||
for d in diags:
|
|
||||||
# don't extract SkipTo elements, they are uninformative as subdiagrams
|
|
||||||
if d.name == "...":
|
|
||||||
continue
|
|
||||||
if d.name is not None and d.name not in seen:
|
|
||||||
seen.add(d.name)
|
|
||||||
deduped_diags.append(d)
|
|
||||||
resolved = [resolve_partial(partial) for partial in deduped_diags]
|
|
||||||
else:
|
|
||||||
# special case - if just one diagram, always display it, even if
|
|
||||||
# it has no name
|
|
||||||
resolved = [resolve_partial(partial) for partial in diags]
|
|
||||||
return sorted(resolved, key=lambda diag: diag.index)
|
|
||||||
|
|
||||||
|
|
||||||
def _should_vertical(
|
|
||||||
specification: int, exprs: Iterable[pyparsing.ParserElement]
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Returns true if we should return a vertical list of elements
|
|
||||||
"""
|
|
||||||
if specification is None:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return len(_visible_exprs(exprs)) >= specification
|
|
||||||
|
|
||||||
|
|
||||||
class ElementState:
|
|
||||||
"""
|
|
||||||
State recorded for an individual pyparsing Element
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Note: this should be a dataclass, but we have to support Python 3.5
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
element: pyparsing.ParserElement,
|
|
||||||
converted: EditablePartial,
|
|
||||||
parent: EditablePartial,
|
|
||||||
number: int,
|
|
||||||
name: str = None,
|
|
||||||
parent_index: typing.Optional[int] = None,
|
|
||||||
):
|
|
||||||
#: The pyparsing element that this represents
|
|
||||||
self.element: pyparsing.ParserElement = element
|
|
||||||
#: The name of the element
|
|
||||||
self.name: typing.Optional[str] = name
|
|
||||||
#: The output Railroad element in an unconverted state
|
|
||||||
self.converted: EditablePartial = converted
|
|
||||||
#: The parent Railroad element, which we store so that we can extract this if it's duplicated
|
|
||||||
self.parent: EditablePartial = parent
|
|
||||||
#: The order in which we found this element, used for sorting diagrams if this is extracted into a diagram
|
|
||||||
self.number: int = number
|
|
||||||
#: The index of this inside its parent
|
|
||||||
self.parent_index: typing.Optional[int] = parent_index
|
|
||||||
#: If true, we should extract this out into a subdiagram
|
|
||||||
self.extract: bool = False
|
|
||||||
#: If true, all of this element's children have been filled out
|
|
||||||
self.complete: bool = False
|
|
||||||
|
|
||||||
def mark_for_extraction(
|
|
||||||
self, el_id: int, state: "ConverterState", name: str = None, force: bool = False
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Called when this instance has been seen twice, and thus should eventually be extracted into a sub-diagram
|
|
||||||
:param el_id: id of the element
|
|
||||||
:param state: element/diagram state tracker
|
|
||||||
:param name: name to use for this element's text
|
|
||||||
:param force: If true, force extraction now, regardless of the state of this. Only useful for extracting the
|
|
||||||
root element when we know we're finished
|
|
||||||
"""
|
|
||||||
self.extract = True
|
|
||||||
|
|
||||||
# Set the name
|
|
||||||
if not self.name:
|
|
||||||
if name:
|
|
||||||
# Allow forcing a custom name
|
|
||||||
self.name = name
|
|
||||||
elif self.element.customName:
|
|
||||||
self.name = self.element.customName
|
|
||||||
else:
|
|
||||||
self.name = ""
|
|
||||||
|
|
||||||
# Just because this is marked for extraction doesn't mean we can do it yet. We may have to wait for children
|
|
||||||
# to be added
|
|
||||||
# Also, if this is just a string literal etc, don't bother extracting it
|
|
||||||
if force or (self.complete and _worth_extracting(self.element)):
|
|
||||||
state.extract_into_diagram(el_id)
|
|
||||||
|
|
||||||
|
|
||||||
class ConverterState:
|
|
||||||
"""
|
|
||||||
Stores some state that persists between recursions into the element tree
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, diagram_kwargs: typing.Optional[dict] = None):
|
|
||||||
#: A dictionary mapping ParserElements to state relating to them
|
|
||||||
self._element_diagram_states: Dict[int, ElementState] = {}
|
|
||||||
#: A dictionary mapping ParserElement IDs to subdiagrams generated from them
|
|
||||||
self.diagrams: Dict[int, EditablePartial[NamedDiagram]] = {}
|
|
||||||
#: The index of the next unnamed element
|
|
||||||
self.unnamed_index: int = 1
|
|
||||||
#: The index of the next element. This is used for sorting
|
|
||||||
self.index: int = 0
|
|
||||||
#: Shared kwargs that are used to customize the construction of diagrams
|
|
||||||
self.diagram_kwargs: dict = diagram_kwargs or {}
|
|
||||||
self.extracted_diagram_names: Set[str] = set()
|
|
||||||
|
|
||||||
def __setitem__(self, key: int, value: ElementState):
|
|
||||||
self._element_diagram_states[key] = value
|
|
||||||
|
|
||||||
def __getitem__(self, key: int) -> ElementState:
|
|
||||||
return self._element_diagram_states[key]
|
|
||||||
|
|
||||||
def __delitem__(self, key: int):
|
|
||||||
del self._element_diagram_states[key]
|
|
||||||
|
|
||||||
def __contains__(self, key: int):
|
|
||||||
return key in self._element_diagram_states
|
|
||||||
|
|
||||||
def generate_unnamed(self) -> int:
|
|
||||||
"""
|
|
||||||
Generate a number used in the name of an otherwise unnamed diagram
|
|
||||||
"""
|
|
||||||
self.unnamed_index += 1
|
|
||||||
return self.unnamed_index
|
|
||||||
|
|
||||||
def generate_index(self) -> int:
|
|
||||||
"""
|
|
||||||
Generate a number used to index a diagram
|
|
||||||
"""
|
|
||||||
self.index += 1
|
|
||||||
return self.index
|
|
||||||
|
|
||||||
def extract_into_diagram(self, el_id: int):
|
|
||||||
"""
|
|
||||||
Used when we encounter the same token twice in the same tree. When this
|
|
||||||
happens, we replace all instances of that token with a terminal, and
|
|
||||||
create a new subdiagram for the token
|
|
||||||
"""
|
|
||||||
position = self[el_id]
|
|
||||||
|
|
||||||
# Replace the original definition of this element with a regular block
|
|
||||||
if position.parent:
|
|
||||||
ret = EditablePartial.from_call(railroad.NonTerminal, text=position.name)
|
|
||||||
if "item" in position.parent.kwargs:
|
|
||||||
position.parent.kwargs["item"] = ret
|
|
||||||
elif "items" in position.parent.kwargs:
|
|
||||||
position.parent.kwargs["items"][position.parent_index] = ret
|
|
||||||
|
|
||||||
# If the element we're extracting is a group, skip to its content but keep the title
|
|
||||||
if position.converted.func == railroad.Group:
|
|
||||||
content = position.converted.kwargs["item"]
|
|
||||||
else:
|
|
||||||
content = position.converted
|
|
||||||
|
|
||||||
self.diagrams[el_id] = EditablePartial.from_call(
|
|
||||||
NamedDiagram,
|
|
||||||
name=position.name,
|
|
||||||
diagram=EditablePartial.from_call(
|
|
||||||
railroad.Diagram, content, **self.diagram_kwargs
|
|
||||||
),
|
|
||||||
index=position.number,
|
|
||||||
)
|
|
||||||
|
|
||||||
del self[el_id]
|
|
||||||
|
|
||||||
|
|
||||||
def _worth_extracting(element: pyparsing.ParserElement) -> bool:
|
|
||||||
"""
|
|
||||||
Returns true if this element is worth having its own sub-diagram. Simply, if any of its children
|
|
||||||
themselves have children, then its complex enough to extract
|
|
||||||
"""
|
|
||||||
children = element.recurse()
|
|
||||||
return any(child.recurse() for child in children)
|
|
||||||
|
|
||||||
|
|
||||||
def _apply_diagram_item_enhancements(fn):
|
|
||||||
"""
|
|
||||||
decorator to ensure enhancements to a diagram item (such as results name annotations)
|
|
||||||
get applied on return from _to_diagram_element (we do this since there are several
|
|
||||||
returns in _to_diagram_element)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _inner(
|
|
||||||
element: pyparsing.ParserElement,
|
|
||||||
parent: typing.Optional[EditablePartial],
|
|
||||||
lookup: ConverterState = None,
|
|
||||||
vertical: int = None,
|
|
||||||
index: int = 0,
|
|
||||||
name_hint: str = None,
|
|
||||||
show_results_names: bool = False,
|
|
||||||
show_groups: bool = False,
|
|
||||||
) -> typing.Optional[EditablePartial]:
|
|
||||||
|
|
||||||
ret = fn(
|
|
||||||
element,
|
|
||||||
parent,
|
|
||||||
lookup,
|
|
||||||
vertical,
|
|
||||||
index,
|
|
||||||
name_hint,
|
|
||||||
show_results_names,
|
|
||||||
show_groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
# apply annotation for results name, if present
|
|
||||||
if show_results_names and ret is not None:
|
|
||||||
element_results_name = element.resultsName
|
|
||||||
if element_results_name:
|
|
||||||
# add "*" to indicate if this is a "list all results" name
|
|
||||||
element_results_name += "" if element.modalResults else "*"
|
|
||||||
ret = EditablePartial.from_call(
|
|
||||||
railroad.Group, item=ret, label=element_results_name
|
|
||||||
)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
return _inner
|
|
||||||
|
|
||||||
|
|
||||||
def _visible_exprs(exprs: Iterable[pyparsing.ParserElement]):
|
|
||||||
non_diagramming_exprs = (
|
|
||||||
pyparsing.ParseElementEnhance,
|
|
||||||
pyparsing.PositionToken,
|
|
||||||
pyparsing.And._ErrorStop,
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
e
|
|
||||||
for e in exprs
|
|
||||||
if not (e.customName or e.resultsName or isinstance(e, non_diagramming_exprs))
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@_apply_diagram_item_enhancements
|
|
||||||
def _to_diagram_element(
|
|
||||||
element: pyparsing.ParserElement,
|
|
||||||
parent: typing.Optional[EditablePartial],
|
|
||||||
lookup: ConverterState = None,
|
|
||||||
vertical: int = None,
|
|
||||||
index: int = 0,
|
|
||||||
name_hint: str = None,
|
|
||||||
show_results_names: bool = False,
|
|
||||||
show_groups: bool = False,
|
|
||||||
) -> typing.Optional[EditablePartial]:
|
|
||||||
"""
|
|
||||||
Recursively converts a PyParsing Element to a railroad Element
|
|
||||||
:param lookup: The shared converter state that keeps track of useful things
|
|
||||||
:param index: The index of this element within the parent
|
|
||||||
:param parent: The parent of this element in the output tree
|
|
||||||
:param vertical: Controls at what point we make a list of elements vertical. If this is an integer (the default),
|
|
||||||
it sets the threshold of the number of items before we go vertical. If True, always go vertical, if False, never
|
|
||||||
do so
|
|
||||||
:param name_hint: If provided, this will override the generated name
|
|
||||||
:param show_results_names: bool flag indicating whether to add annotations for results names
|
|
||||||
:returns: The converted version of the input element, but as a Partial that hasn't yet been constructed
|
|
||||||
:param show_groups: bool flag indicating whether to show groups using bounding box
|
|
||||||
"""
|
|
||||||
exprs = element.recurse()
|
|
||||||
name = name_hint or element.customName or element.__class__.__name__
|
|
||||||
|
|
||||||
# Python's id() is used to provide a unique identifier for elements
|
|
||||||
el_id = id(element)
|
|
||||||
|
|
||||||
element_results_name = element.resultsName
|
|
||||||
|
|
||||||
# Here we basically bypass processing certain wrapper elements if they contribute nothing to the diagram
|
|
||||||
if not element.customName:
|
|
||||||
if isinstance(
|
|
||||||
element,
|
|
||||||
(
|
|
||||||
# pyparsing.TokenConverter,
|
|
||||||
# pyparsing.Forward,
|
|
||||||
pyparsing.Located,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
# However, if this element has a useful custom name, and its child does not, we can pass it on to the child
|
|
||||||
if exprs:
|
|
||||||
if not exprs[0].customName:
|
|
||||||
propagated_name = name
|
|
||||||
else:
|
|
||||||
propagated_name = None
|
|
||||||
|
|
||||||
return _to_diagram_element(
|
|
||||||
element.expr,
|
|
||||||
parent=parent,
|
|
||||||
lookup=lookup,
|
|
||||||
vertical=vertical,
|
|
||||||
index=index,
|
|
||||||
name_hint=propagated_name,
|
|
||||||
show_results_names=show_results_names,
|
|
||||||
show_groups=show_groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the element isn't worth extracting, we always treat it as the first time we say it
|
|
||||||
if _worth_extracting(element):
|
|
||||||
if el_id in lookup:
|
|
||||||
# If we've seen this element exactly once before, we are only just now finding out that it's a duplicate,
|
|
||||||
# so we have to extract it into a new diagram.
|
|
||||||
looked_up = lookup[el_id]
|
|
||||||
looked_up.mark_for_extraction(el_id, lookup, name=name_hint)
|
|
||||||
ret = EditablePartial.from_call(railroad.NonTerminal, text=looked_up.name)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
elif el_id in lookup.diagrams:
|
|
||||||
# If we have seen the element at least twice before, and have already extracted it into a subdiagram, we
|
|
||||||
# just put in a marker element that refers to the sub-diagram
|
|
||||||
ret = EditablePartial.from_call(
|
|
||||||
railroad.NonTerminal, text=lookup.diagrams[el_id].kwargs["name"]
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
# Recursively convert child elements
|
|
||||||
# Here we find the most relevant Railroad element for matching pyparsing Element
|
|
||||||
# We use ``items=[]`` here to hold the place for where the child elements will go once created
|
|
||||||
if isinstance(element, pyparsing.And):
|
|
||||||
# detect And's created with ``expr*N`` notation - for these use a OneOrMore with a repeat
|
|
||||||
# (all will have the same name, and resultsName)
|
|
||||||
if not exprs:
|
|
||||||
return None
|
|
||||||
if len(set((e.name, e.resultsName) for e in exprs)) == 1:
|
|
||||||
ret = EditablePartial.from_call(
|
|
||||||
railroad.OneOrMore, item="", repeat=str(len(exprs))
|
|
||||||
)
|
|
||||||
elif _should_vertical(vertical, exprs):
|
|
||||||
ret = EditablePartial.from_call(railroad.Stack, items=[])
|
|
||||||
else:
|
|
||||||
ret = EditablePartial.from_call(railroad.Sequence, items=[])
|
|
||||||
elif isinstance(element, (pyparsing.Or, pyparsing.MatchFirst)):
|
|
||||||
if not exprs:
|
|
||||||
return None
|
|
||||||
if _should_vertical(vertical, exprs):
|
|
||||||
ret = EditablePartial.from_call(railroad.Choice, 0, items=[])
|
|
||||||
else:
|
|
||||||
ret = EditablePartial.from_call(railroad.HorizontalChoice, items=[])
|
|
||||||
elif isinstance(element, pyparsing.Each):
|
|
||||||
if not exprs:
|
|
||||||
return None
|
|
||||||
ret = EditablePartial.from_call(EachItem, items=[])
|
|
||||||
elif isinstance(element, pyparsing.NotAny):
|
|
||||||
ret = EditablePartial.from_call(AnnotatedItem, label="NOT", item="")
|
|
||||||
elif isinstance(element, pyparsing.FollowedBy):
|
|
||||||
ret = EditablePartial.from_call(AnnotatedItem, label="LOOKAHEAD", item="")
|
|
||||||
elif isinstance(element, pyparsing.PrecededBy):
|
|
||||||
ret = EditablePartial.from_call(AnnotatedItem, label="LOOKBEHIND", item="")
|
|
||||||
elif isinstance(element, pyparsing.Group):
|
|
||||||
if show_groups:
|
|
||||||
ret = EditablePartial.from_call(AnnotatedItem, label="", item="")
|
|
||||||
else:
|
|
||||||
ret = EditablePartial.from_call(railroad.Group, label="", item="")
|
|
||||||
elif isinstance(element, pyparsing.TokenConverter):
|
|
||||||
ret = EditablePartial.from_call(
|
|
||||||
AnnotatedItem, label=type(element).__name__.lower(), item=""
|
|
||||||
)
|
|
||||||
elif isinstance(element, pyparsing.Opt):
|
|
||||||
ret = EditablePartial.from_call(railroad.Optional, item="")
|
|
||||||
elif isinstance(element, pyparsing.OneOrMore):
|
|
||||||
ret = EditablePartial.from_call(railroad.OneOrMore, item="")
|
|
||||||
elif isinstance(element, pyparsing.ZeroOrMore):
|
|
||||||
ret = EditablePartial.from_call(railroad.ZeroOrMore, item="")
|
|
||||||
elif isinstance(element, pyparsing.Group):
|
|
||||||
ret = EditablePartial.from_call(
|
|
||||||
railroad.Group, item=None, label=element_results_name
|
|
||||||
)
|
|
||||||
elif isinstance(element, pyparsing.Empty) and not element.customName:
|
|
||||||
# Skip unnamed "Empty" elements
|
|
||||||
ret = None
|
|
||||||
elif len(exprs) > 1:
|
|
||||||
ret = EditablePartial.from_call(railroad.Sequence, items=[])
|
|
||||||
elif len(exprs) > 0 and not element_results_name:
|
|
||||||
ret = EditablePartial.from_call(railroad.Group, item="", label=name)
|
|
||||||
else:
|
|
||||||
terminal = EditablePartial.from_call(railroad.Terminal, element.defaultName)
|
|
||||||
ret = terminal
|
|
||||||
|
|
||||||
if ret is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Indicate this element's position in the tree so we can extract it if necessary
|
|
||||||
lookup[el_id] = ElementState(
|
|
||||||
element=element,
|
|
||||||
converted=ret,
|
|
||||||
parent=parent,
|
|
||||||
parent_index=index,
|
|
||||||
number=lookup.generate_index(),
|
|
||||||
)
|
|
||||||
if element.customName:
|
|
||||||
lookup[el_id].mark_for_extraction(el_id, lookup, element.customName)
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
for expr in exprs:
|
|
||||||
# Add a placeholder index in case we have to extract the child before we even add it to the parent
|
|
||||||
if "items" in ret.kwargs:
|
|
||||||
ret.kwargs["items"].insert(i, None)
|
|
||||||
|
|
||||||
item = _to_diagram_element(
|
|
||||||
expr,
|
|
||||||
parent=ret,
|
|
||||||
lookup=lookup,
|
|
||||||
vertical=vertical,
|
|
||||||
index=i,
|
|
||||||
show_results_names=show_results_names,
|
|
||||||
show_groups=show_groups,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Some elements don't need to be shown in the diagram
|
|
||||||
if item is not None:
|
|
||||||
if "item" in ret.kwargs:
|
|
||||||
ret.kwargs["item"] = item
|
|
||||||
elif "items" in ret.kwargs:
|
|
||||||
# If we've already extracted the child, don't touch this index, since it's occupied by a nonterminal
|
|
||||||
ret.kwargs["items"][i] = item
|
|
||||||
i += 1
|
|
||||||
elif "items" in ret.kwargs:
|
|
||||||
# If we're supposed to skip this element, remove it from the parent
|
|
||||||
del ret.kwargs["items"][i]
|
|
||||||
|
|
||||||
# If all this items children are none, skip this item
|
|
||||||
if ret and (
|
|
||||||
("items" in ret.kwargs and len(ret.kwargs["items"]) == 0)
|
|
||||||
or ("item" in ret.kwargs and ret.kwargs["item"] is None)
|
|
||||||
):
|
|
||||||
ret = EditablePartial.from_call(railroad.Terminal, name)
|
|
||||||
|
|
||||||
# Mark this element as "complete", ie it has all of its children
|
|
||||||
if el_id in lookup:
|
|
||||||
lookup[el_id].complete = True
|
|
||||||
|
|
||||||
if el_id in lookup and lookup[el_id].extract and lookup[el_id].complete:
|
|
||||||
lookup.extract_into_diagram(el_id)
|
|
||||||
if ret is not None:
|
|
||||||
ret = EditablePartial.from_call(
|
|
||||||
railroad.NonTerminal, text=lookup.diagrams[el_id].kwargs["name"]
|
|
||||||
)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
BIN
Binary file not shown.
@@ -1,267 +0,0 @@
|
|||||||
# exceptions.py
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .util import col, line, lineno, _collapse_string_to_ranges
|
|
||||||
from .unicode import pyparsing_unicode as ppu
|
|
||||||
|
|
||||||
|
|
||||||
class ExceptionWordUnicode(ppu.Latin1, ppu.LatinA, ppu.LatinB, ppu.Greek, ppu.Cyrillic):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_extract_alphanums = _collapse_string_to_ranges(ExceptionWordUnicode.alphanums)
|
|
||||||
_exception_word_extractor = re.compile("([" + _extract_alphanums + "]{1,16})|.")
|
|
||||||
|
|
||||||
|
|
||||||
class ParseBaseException(Exception):
|
|
||||||
"""base exception class for all parsing runtime exceptions"""
|
|
||||||
|
|
||||||
# Performance tuning: we construct a *lot* of these, so keep this
|
|
||||||
# constructor as small and fast as possible
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
pstr: str,
|
|
||||||
loc: int = 0,
|
|
||||||
msg: typing.Optional[str] = None,
|
|
||||||
elem=None,
|
|
||||||
):
|
|
||||||
self.loc = loc
|
|
||||||
if msg is None:
|
|
||||||
self.msg = pstr
|
|
||||||
self.pstr = ""
|
|
||||||
else:
|
|
||||||
self.msg = msg
|
|
||||||
self.pstr = pstr
|
|
||||||
self.parser_element = self.parserElement = elem
|
|
||||||
self.args = (pstr, loc, msg)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def explain_exception(exc, depth=16):
|
|
||||||
"""
|
|
||||||
Method to take an exception and translate the Python internal traceback into a list
|
|
||||||
of the pyparsing expressions that caused the exception to be raised.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
|
|
||||||
- exc - exception raised during parsing (need not be a ParseException, in support
|
|
||||||
of Python exceptions that might be raised in a parse action)
|
|
||||||
- depth (default=16) - number of levels back in the stack trace to list expression
|
|
||||||
and function names; if None, the full stack trace names will be listed; if 0, only
|
|
||||||
the failing input line, marker, and exception string will be shown
|
|
||||||
|
|
||||||
Returns a multi-line string listing the ParserElements and/or function names in the
|
|
||||||
exception's stack trace.
|
|
||||||
"""
|
|
||||||
import inspect
|
|
||||||
from .core import ParserElement
|
|
||||||
|
|
||||||
if depth is None:
|
|
||||||
depth = sys.getrecursionlimit()
|
|
||||||
ret = []
|
|
||||||
if isinstance(exc, ParseBaseException):
|
|
||||||
ret.append(exc.line)
|
|
||||||
ret.append(" " * (exc.column - 1) + "^")
|
|
||||||
ret.append("{}: {}".format(type(exc).__name__, exc))
|
|
||||||
|
|
||||||
if depth > 0:
|
|
||||||
callers = inspect.getinnerframes(exc.__traceback__, context=depth)
|
|
||||||
seen = set()
|
|
||||||
for i, ff in enumerate(callers[-depth:]):
|
|
||||||
frm = ff[0]
|
|
||||||
|
|
||||||
f_self = frm.f_locals.get("self", None)
|
|
||||||
if isinstance(f_self, ParserElement):
|
|
||||||
if frm.f_code.co_name not in ("parseImpl", "_parseNoCache"):
|
|
||||||
continue
|
|
||||||
if id(f_self) in seen:
|
|
||||||
continue
|
|
||||||
seen.add(id(f_self))
|
|
||||||
|
|
||||||
self_type = type(f_self)
|
|
||||||
ret.append(
|
|
||||||
"{}.{} - {}".format(
|
|
||||||
self_type.__module__, self_type.__name__, f_self
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
elif f_self is not None:
|
|
||||||
self_type = type(f_self)
|
|
||||||
ret.append("{}.{}".format(self_type.__module__, self_type.__name__))
|
|
||||||
|
|
||||||
else:
|
|
||||||
code = frm.f_code
|
|
||||||
if code.co_name in ("wrapper", "<module>"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
ret.append("{}".format(code.co_name))
|
|
||||||
|
|
||||||
depth -= 1
|
|
||||||
if not depth:
|
|
||||||
break
|
|
||||||
|
|
||||||
return "\n".join(ret)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _from_exception(cls, pe):
|
|
||||||
"""
|
|
||||||
internal factory method to simplify creating one type of ParseException
|
|
||||||
from another - avoids having __init__ signature conflicts among subclasses
|
|
||||||
"""
|
|
||||||
return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def line(self) -> str:
|
|
||||||
"""
|
|
||||||
Return the line of text where the exception occurred.
|
|
||||||
"""
|
|
||||||
return line(self.loc, self.pstr)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lineno(self) -> int:
|
|
||||||
"""
|
|
||||||
Return the 1-based line number of text where the exception occurred.
|
|
||||||
"""
|
|
||||||
return lineno(self.loc, self.pstr)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def col(self) -> int:
|
|
||||||
"""
|
|
||||||
Return the 1-based column on the line of text where the exception occurred.
|
|
||||||
"""
|
|
||||||
return col(self.loc, self.pstr)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def column(self) -> int:
|
|
||||||
"""
|
|
||||||
Return the 1-based column on the line of text where the exception occurred.
|
|
||||||
"""
|
|
||||||
return col(self.loc, self.pstr)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
if self.pstr:
|
|
||||||
if self.loc >= len(self.pstr):
|
|
||||||
foundstr = ", found end of text"
|
|
||||||
else:
|
|
||||||
# pull out next word at error location
|
|
||||||
found_match = _exception_word_extractor.match(self.pstr, self.loc)
|
|
||||||
if found_match is not None:
|
|
||||||
found = found_match.group(0)
|
|
||||||
else:
|
|
||||||
found = self.pstr[self.loc : self.loc + 1]
|
|
||||||
foundstr = (", found %r" % found).replace(r"\\", "\\")
|
|
||||||
else:
|
|
||||||
foundstr = ""
|
|
||||||
return "{}{} (at char {}), (line:{}, col:{})".format(
|
|
||||||
self.msg, foundstr, self.loc, self.lineno, self.column
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return str(self)
|
|
||||||
|
|
||||||
def mark_input_line(self, marker_string: str = None, *, markerString=">!<") -> str:
|
|
||||||
"""
|
|
||||||
Extracts the exception line from the input string, and marks
|
|
||||||
the location of the exception with a special symbol.
|
|
||||||
"""
|
|
||||||
markerString = marker_string if marker_string is not None else markerString
|
|
||||||
line_str = self.line
|
|
||||||
line_column = self.column - 1
|
|
||||||
if markerString:
|
|
||||||
line_str = "".join(
|
|
||||||
(line_str[:line_column], markerString, line_str[line_column:])
|
|
||||||
)
|
|
||||||
return line_str.strip()
|
|
||||||
|
|
||||||
def explain(self, depth=16) -> str:
|
|
||||||
"""
|
|
||||||
Method to translate the Python internal traceback into a list
|
|
||||||
of the pyparsing expressions that caused the exception to be raised.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
|
|
||||||
- depth (default=16) - number of levels back in the stack trace to list expression
|
|
||||||
and function names; if None, the full stack trace names will be listed; if 0, only
|
|
||||||
the failing input line, marker, and exception string will be shown
|
|
||||||
|
|
||||||
Returns a multi-line string listing the ParserElements and/or function names in the
|
|
||||||
exception's stack trace.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
expr = pp.Word(pp.nums) * 3
|
|
||||||
try:
|
|
||||||
expr.parse_string("123 456 A789")
|
|
||||||
except pp.ParseException as pe:
|
|
||||||
print(pe.explain(depth=0))
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
123 456 A789
|
|
||||||
^
|
|
||||||
ParseException: Expected W:(0-9), found 'A' (at char 8), (line:1, col:9)
|
|
||||||
|
|
||||||
Note: the diagnostic output will include string representations of the expressions
|
|
||||||
that failed to parse. These representations will be more helpful if you use `set_name` to
|
|
||||||
give identifiable names to your expressions. Otherwise they will use the default string
|
|
||||||
forms, which may be cryptic to read.
|
|
||||||
|
|
||||||
Note: pyparsing's default truncation of exception tracebacks may also truncate the
|
|
||||||
stack of expressions that are displayed in the ``explain`` output. To get the full listing
|
|
||||||
of parser expressions, you may have to set ``ParserElement.verbose_stacktrace = True``
|
|
||||||
"""
|
|
||||||
return self.explain_exception(self, depth)
|
|
||||||
|
|
||||||
markInputline = mark_input_line
|
|
||||||
|
|
||||||
|
|
||||||
class ParseException(ParseBaseException):
|
|
||||||
"""
|
|
||||||
Exception thrown when a parse expression doesn't match the input string
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
try:
|
|
||||||
Word(nums).set_name("integer").parse_string("ABC")
|
|
||||||
except ParseException as pe:
|
|
||||||
print(pe)
|
|
||||||
print("column: {}".format(pe.column))
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
Expected integer (at char 0), (line:1, col:1)
|
|
||||||
column: 1
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ParseFatalException(ParseBaseException):
|
|
||||||
"""
|
|
||||||
User-throwable exception thrown when inconsistent parse content
|
|
||||||
is found; stops all parsing immediately
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ParseSyntaxException(ParseFatalException):
|
|
||||||
"""
|
|
||||||
Just like :class:`ParseFatalException`, but thrown internally
|
|
||||||
when an :class:`ErrorStop<And._ErrorStop>` ('-' operator) indicates
|
|
||||||
that parsing is to stop immediately because an unbacktrackable
|
|
||||||
syntax error has been found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class RecursiveGrammarException(Exception):
|
|
||||||
"""
|
|
||||||
Exception thrown by :class:`ParserElement.validate` if the
|
|
||||||
grammar could be left-recursive; parser may need to enable
|
|
||||||
left recursion using :class:`ParserElement.enable_left_recursion<ParserElement.enable_left_recursion>`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parseElementList):
|
|
||||||
self.parseElementTrace = parseElementList
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "RecursiveGrammarException: {}".format(self.parseElementTrace)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,760 +0,0 @@
|
|||||||
# results.py
|
|
||||||
from collections.abc import MutableMapping, Mapping, MutableSequence, Iterator
|
|
||||||
import pprint
|
|
||||||
from weakref import ref as wkref
|
|
||||||
from typing import Tuple, Any
|
|
||||||
|
|
||||||
str_type: Tuple[type, ...] = (str, bytes)
|
|
||||||
_generator_type = type((_ for _ in ()))
|
|
||||||
|
|
||||||
|
|
||||||
class _ParseResultsWithOffset:
|
|
||||||
__slots__ = ["tup"]
|
|
||||||
|
|
||||||
def __init__(self, p1, p2):
|
|
||||||
self.tup = (p1, p2)
|
|
||||||
|
|
||||||
def __getitem__(self, i):
|
|
||||||
return self.tup[i]
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
return self.tup
|
|
||||||
|
|
||||||
def __setstate__(self, *args):
|
|
||||||
self.tup = args[0]
|
|
||||||
|
|
||||||
|
|
||||||
class ParseResults:
|
|
||||||
"""Structured parse results, to provide multiple means of access to
|
|
||||||
the parsed data:
|
|
||||||
|
|
||||||
- as a list (``len(results)``)
|
|
||||||
- by list index (``results[0], results[1]``, etc.)
|
|
||||||
- by attribute (``results.<results_name>`` - see :class:`ParserElement.set_results_name`)
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
integer = Word(nums)
|
|
||||||
date_str = (integer.set_results_name("year") + '/'
|
|
||||||
+ integer.set_results_name("month") + '/'
|
|
||||||
+ integer.set_results_name("day"))
|
|
||||||
# equivalent form:
|
|
||||||
# date_str = (integer("year") + '/'
|
|
||||||
# + integer("month") + '/'
|
|
||||||
# + integer("day"))
|
|
||||||
|
|
||||||
# parse_string returns a ParseResults object
|
|
||||||
result = date_str.parse_string("1999/12/31")
|
|
||||||
|
|
||||||
def test(s, fn=repr):
|
|
||||||
print("{} -> {}".format(s, fn(eval(s))))
|
|
||||||
test("list(result)")
|
|
||||||
test("result[0]")
|
|
||||||
test("result['month']")
|
|
||||||
test("result.day")
|
|
||||||
test("'month' in result")
|
|
||||||
test("'minutes' in result")
|
|
||||||
test("result.dump()", str)
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
list(result) -> ['1999', '/', '12', '/', '31']
|
|
||||||
result[0] -> '1999'
|
|
||||||
result['month'] -> '12'
|
|
||||||
result.day -> '31'
|
|
||||||
'month' in result -> True
|
|
||||||
'minutes' in result -> False
|
|
||||||
result.dump() -> ['1999', '/', '12', '/', '31']
|
|
||||||
- day: '31'
|
|
||||||
- month: '12'
|
|
||||||
- year: '1999'
|
|
||||||
"""
|
|
||||||
|
|
||||||
_null_values: Tuple[Any, ...] = (None, [], "", ())
|
|
||||||
|
|
||||||
__slots__ = [
|
|
||||||
"_name",
|
|
||||||
"_parent",
|
|
||||||
"_all_names",
|
|
||||||
"_modal",
|
|
||||||
"_toklist",
|
|
||||||
"_tokdict",
|
|
||||||
"__weakref__",
|
|
||||||
]
|
|
||||||
|
|
||||||
class List(list):
|
|
||||||
"""
|
|
||||||
Simple wrapper class to distinguish parsed list results that should be preserved
|
|
||||||
as actual Python lists, instead of being converted to :class:`ParseResults`:
|
|
||||||
|
|
||||||
LBRACK, RBRACK = map(pp.Suppress, "[]")
|
|
||||||
element = pp.Forward()
|
|
||||||
item = ppc.integer
|
|
||||||
element_list = LBRACK + pp.delimited_list(element) + RBRACK
|
|
||||||
|
|
||||||
# add parse actions to convert from ParseResults to actual Python collection types
|
|
||||||
def as_python_list(t):
|
|
||||||
return pp.ParseResults.List(t.as_list())
|
|
||||||
element_list.add_parse_action(as_python_list)
|
|
||||||
|
|
||||||
element <<= item | element_list
|
|
||||||
|
|
||||||
element.run_tests('''
|
|
||||||
100
|
|
||||||
[2,3,4]
|
|
||||||
[[2, 1],3,4]
|
|
||||||
[(2, 1),3,4]
|
|
||||||
(2,3,4)
|
|
||||||
''', post_parse=lambda s, r: (r[0], type(r[0])))
|
|
||||||
|
|
||||||
prints:
|
|
||||||
|
|
||||||
100
|
|
||||||
(100, <class 'int'>)
|
|
||||||
|
|
||||||
[2,3,4]
|
|
||||||
([2, 3, 4], <class 'list'>)
|
|
||||||
|
|
||||||
[[2, 1],3,4]
|
|
||||||
([[2, 1], 3, 4], <class 'list'>)
|
|
||||||
|
|
||||||
(Used internally by :class:`Group` when `aslist=True`.)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __new__(cls, contained=None):
|
|
||||||
if contained is None:
|
|
||||||
contained = []
|
|
||||||
|
|
||||||
if not isinstance(contained, list):
|
|
||||||
raise TypeError(
|
|
||||||
"{} may only be constructed with a list,"
|
|
||||||
" not {}".format(cls.__name__, type(contained).__name__)
|
|
||||||
)
|
|
||||||
|
|
||||||
return list.__new__(cls)
|
|
||||||
|
|
||||||
def __new__(cls, toklist=None, name=None, **kwargs):
|
|
||||||
if isinstance(toklist, ParseResults):
|
|
||||||
return toklist
|
|
||||||
self = object.__new__(cls)
|
|
||||||
self._name = None
|
|
||||||
self._parent = None
|
|
||||||
self._all_names = set()
|
|
||||||
|
|
||||||
if toklist is None:
|
|
||||||
self._toklist = []
|
|
||||||
elif isinstance(toklist, (list, _generator_type)):
|
|
||||||
self._toklist = (
|
|
||||||
[toklist[:]]
|
|
||||||
if isinstance(toklist, ParseResults.List)
|
|
||||||
else list(toklist)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._toklist = [toklist]
|
|
||||||
self._tokdict = dict()
|
|
||||||
return self
|
|
||||||
|
|
||||||
# Performance tuning: we construct a *lot* of these, so keep this
|
|
||||||
# constructor as small and fast as possible
|
|
||||||
def __init__(
|
|
||||||
self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance
|
|
||||||
):
|
|
||||||
self._modal = modal
|
|
||||||
if name is not None and name != "":
|
|
||||||
if isinstance(name, int):
|
|
||||||
name = str(name)
|
|
||||||
if not modal:
|
|
||||||
self._all_names = {name}
|
|
||||||
self._name = name
|
|
||||||
if toklist not in self._null_values:
|
|
||||||
if isinstance(toklist, (str_type, type)):
|
|
||||||
toklist = [toklist]
|
|
||||||
if asList:
|
|
||||||
if isinstance(toklist, ParseResults):
|
|
||||||
self[name] = _ParseResultsWithOffset(
|
|
||||||
ParseResults(toklist._toklist), 0
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self[name] = _ParseResultsWithOffset(
|
|
||||||
ParseResults(toklist[0]), 0
|
|
||||||
)
|
|
||||||
self[name]._name = name
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self[name] = toklist[0]
|
|
||||||
except (KeyError, TypeError, IndexError):
|
|
||||||
if toklist is not self:
|
|
||||||
self[name] = toklist
|
|
||||||
else:
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
def __getitem__(self, i):
|
|
||||||
if isinstance(i, (int, slice)):
|
|
||||||
return self._toklist[i]
|
|
||||||
else:
|
|
||||||
if i not in self._all_names:
|
|
||||||
return self._tokdict[i][-1][0]
|
|
||||||
else:
|
|
||||||
return ParseResults([v[0] for v in self._tokdict[i]])
|
|
||||||
|
|
||||||
def __setitem__(self, k, v, isinstance=isinstance):
|
|
||||||
if isinstance(v, _ParseResultsWithOffset):
|
|
||||||
self._tokdict[k] = self._tokdict.get(k, list()) + [v]
|
|
||||||
sub = v[0]
|
|
||||||
elif isinstance(k, (int, slice)):
|
|
||||||
self._toklist[k] = v
|
|
||||||
sub = v
|
|
||||||
else:
|
|
||||||
self._tokdict[k] = self._tokdict.get(k, list()) + [
|
|
||||||
_ParseResultsWithOffset(v, 0)
|
|
||||||
]
|
|
||||||
sub = v
|
|
||||||
if isinstance(sub, ParseResults):
|
|
||||||
sub._parent = wkref(self)
|
|
||||||
|
|
||||||
def __delitem__(self, i):
|
|
||||||
if isinstance(i, (int, slice)):
|
|
||||||
mylen = len(self._toklist)
|
|
||||||
del self._toklist[i]
|
|
||||||
|
|
||||||
# convert int to slice
|
|
||||||
if isinstance(i, int):
|
|
||||||
if i < 0:
|
|
||||||
i += mylen
|
|
||||||
i = slice(i, i + 1)
|
|
||||||
# get removed indices
|
|
||||||
removed = list(range(*i.indices(mylen)))
|
|
||||||
removed.reverse()
|
|
||||||
# fixup indices in token dictionary
|
|
||||||
for name, occurrences in self._tokdict.items():
|
|
||||||
for j in removed:
|
|
||||||
for k, (value, position) in enumerate(occurrences):
|
|
||||||
occurrences[k] = _ParseResultsWithOffset(
|
|
||||||
value, position - (position > j)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
del self._tokdict[i]
|
|
||||||
|
|
||||||
def __contains__(self, k) -> bool:
|
|
||||||
return k in self._tokdict
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._toklist)
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
|
||||||
return not not (self._toklist or self._tokdict)
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator:
|
|
||||||
return iter(self._toklist)
|
|
||||||
|
|
||||||
def __reversed__(self) -> Iterator:
|
|
||||||
return iter(self._toklist[::-1])
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
return iter(self._tokdict)
|
|
||||||
|
|
||||||
def values(self):
|
|
||||||
return (self[k] for k in self.keys())
|
|
||||||
|
|
||||||
def items(self):
|
|
||||||
return ((k, self[k]) for k in self.keys())
|
|
||||||
|
|
||||||
def haskeys(self) -> bool:
|
|
||||||
"""
|
|
||||||
Since ``keys()`` returns an iterator, this method is helpful in bypassing
|
|
||||||
code that looks for the existence of any defined results names."""
|
|
||||||
return bool(self._tokdict)
|
|
||||||
|
|
||||||
def pop(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Removes and returns item at specified index (default= ``last``).
|
|
||||||
Supports both ``list`` and ``dict`` semantics for ``pop()``. If
|
|
||||||
passed no argument or an integer argument, it will use ``list``
|
|
||||||
semantics and pop tokens from the list of parsed tokens. If passed
|
|
||||||
a non-integer argument (most likely a string), it will use ``dict``
|
|
||||||
semantics and pop the corresponding value from any defined results
|
|
||||||
names. A second default return value argument is supported, just as in
|
|
||||||
``dict.pop()``.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
numlist = Word(nums)[...]
|
|
||||||
print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
|
|
||||||
|
|
||||||
def remove_first(tokens):
|
|
||||||
tokens.pop(0)
|
|
||||||
numlist.add_parse_action(remove_first)
|
|
||||||
print(numlist.parse_string("0 123 321")) # -> ['123', '321']
|
|
||||||
|
|
||||||
label = Word(alphas)
|
|
||||||
patt = label("LABEL") + Word(nums)[1, ...]
|
|
||||||
print(patt.parse_string("AAB 123 321").dump())
|
|
||||||
|
|
||||||
# Use pop() in a parse action to remove named result (note that corresponding value is not
|
|
||||||
# removed from list form of results)
|
|
||||||
def remove_LABEL(tokens):
|
|
||||||
tokens.pop("LABEL")
|
|
||||||
return tokens
|
|
||||||
patt.add_parse_action(remove_LABEL)
|
|
||||||
print(patt.parse_string("AAB 123 321").dump())
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
['AAB', '123', '321']
|
|
||||||
- LABEL: 'AAB'
|
|
||||||
|
|
||||||
['AAB', '123', '321']
|
|
||||||
"""
|
|
||||||
if not args:
|
|
||||||
args = [-1]
|
|
||||||
for k, v in kwargs.items():
|
|
||||||
if k == "default":
|
|
||||||
args = (args[0], v)
|
|
||||||
else:
|
|
||||||
raise TypeError(
|
|
||||||
"pop() got an unexpected keyword argument {!r}".format(k)
|
|
||||||
)
|
|
||||||
if isinstance(args[0], int) or len(args) == 1 or args[0] in self:
|
|
||||||
index = args[0]
|
|
||||||
ret = self[index]
|
|
||||||
del self[index]
|
|
||||||
return ret
|
|
||||||
else:
|
|
||||||
defaultvalue = args[1]
|
|
||||||
return defaultvalue
|
|
||||||
|
|
||||||
def get(self, key, default_value=None):
|
|
||||||
"""
|
|
||||||
Returns named result matching the given key, or if there is no
|
|
||||||
such name, then returns the given ``default_value`` or ``None`` if no
|
|
||||||
``default_value`` is specified.
|
|
||||||
|
|
||||||
Similar to ``dict.get()``.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
integer = Word(nums)
|
|
||||||
date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
|
|
||||||
|
|
||||||
result = date_str.parse_string("1999/12/31")
|
|
||||||
print(result.get("year")) # -> '1999'
|
|
||||||
print(result.get("hour", "not specified")) # -> 'not specified'
|
|
||||||
print(result.get("hour")) # -> None
|
|
||||||
"""
|
|
||||||
if key in self:
|
|
||||||
return self[key]
|
|
||||||
else:
|
|
||||||
return default_value
|
|
||||||
|
|
||||||
def insert(self, index, ins_string):
|
|
||||||
"""
|
|
||||||
Inserts new element at location index in the list of parsed tokens.
|
|
||||||
|
|
||||||
Similar to ``list.insert()``.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
numlist = Word(nums)[...]
|
|
||||||
print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
|
|
||||||
|
|
||||||
# use a parse action to insert the parse location in the front of the parsed results
|
|
||||||
def insert_locn(locn, tokens):
|
|
||||||
tokens.insert(0, locn)
|
|
||||||
numlist.add_parse_action(insert_locn)
|
|
||||||
print(numlist.parse_string("0 123 321")) # -> [0, '0', '123', '321']
|
|
||||||
"""
|
|
||||||
self._toklist.insert(index, ins_string)
|
|
||||||
# fixup indices in token dictionary
|
|
||||||
for name, occurrences in self._tokdict.items():
|
|
||||||
for k, (value, position) in enumerate(occurrences):
|
|
||||||
occurrences[k] = _ParseResultsWithOffset(
|
|
||||||
value, position + (position > index)
|
|
||||||
)
|
|
||||||
|
|
||||||
def append(self, item):
|
|
||||||
"""
|
|
||||||
Add single element to end of ``ParseResults`` list of elements.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
numlist = Word(nums)[...]
|
|
||||||
print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321']
|
|
||||||
|
|
||||||
# use a parse action to compute the sum of the parsed integers, and add it to the end
|
|
||||||
def append_sum(tokens):
|
|
||||||
tokens.append(sum(map(int, tokens)))
|
|
||||||
numlist.add_parse_action(append_sum)
|
|
||||||
print(numlist.parse_string("0 123 321")) # -> ['0', '123', '321', 444]
|
|
||||||
"""
|
|
||||||
self._toklist.append(item)
|
|
||||||
|
|
||||||
def extend(self, itemseq):
|
|
||||||
"""
|
|
||||||
Add sequence of elements to end of ``ParseResults`` list of elements.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
patt = Word(alphas)[1, ...]
|
|
||||||
|
|
||||||
# use a parse action to append the reverse of the matched strings, to make a palindrome
|
|
||||||
def make_palindrome(tokens):
|
|
||||||
tokens.extend(reversed([t[::-1] for t in tokens]))
|
|
||||||
return ''.join(tokens)
|
|
||||||
patt.add_parse_action(make_palindrome)
|
|
||||||
print(patt.parse_string("lskdj sdlkjf lksd")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'
|
|
||||||
"""
|
|
||||||
if isinstance(itemseq, ParseResults):
|
|
||||||
self.__iadd__(itemseq)
|
|
||||||
else:
|
|
||||||
self._toklist.extend(itemseq)
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""
|
|
||||||
Clear all elements and results names.
|
|
||||||
"""
|
|
||||||
del self._toklist[:]
|
|
||||||
self._tokdict.clear()
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
try:
|
|
||||||
return self[name]
|
|
||||||
except KeyError:
|
|
||||||
if name.startswith("__"):
|
|
||||||
raise AttributeError(name)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def __add__(self, other) -> "ParseResults":
|
|
||||||
ret = self.copy()
|
|
||||||
ret += other
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __iadd__(self, other) -> "ParseResults":
|
|
||||||
if other._tokdict:
|
|
||||||
offset = len(self._toklist)
|
|
||||||
addoffset = lambda a: offset if a < 0 else a + offset
|
|
||||||
otheritems = other._tokdict.items()
|
|
||||||
otherdictitems = [
|
|
||||||
(k, _ParseResultsWithOffset(v[0], addoffset(v[1])))
|
|
||||||
for k, vlist in otheritems
|
|
||||||
for v in vlist
|
|
||||||
]
|
|
||||||
for k, v in otherdictitems:
|
|
||||||
self[k] = v
|
|
||||||
if isinstance(v[0], ParseResults):
|
|
||||||
v[0]._parent = wkref(self)
|
|
||||||
|
|
||||||
self._toklist += other._toklist
|
|
||||||
self._all_names |= other._all_names
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __radd__(self, other) -> "ParseResults":
|
|
||||||
if isinstance(other, int) and other == 0:
|
|
||||||
# useful for merging many ParseResults using sum() builtin
|
|
||||||
return self.copy()
|
|
||||||
else:
|
|
||||||
# this may raise a TypeError - so be it
|
|
||||||
return other + self
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return "{}({!r}, {})".format(type(self).__name__, self._toklist, self.as_dict())
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return (
|
|
||||||
"["
|
|
||||||
+ ", ".join(
|
|
||||||
[
|
|
||||||
str(i) if isinstance(i, ParseResults) else repr(i)
|
|
||||||
for i in self._toklist
|
|
||||||
]
|
|
||||||
)
|
|
||||||
+ "]"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _asStringList(self, sep=""):
|
|
||||||
out = []
|
|
||||||
for item in self._toklist:
|
|
||||||
if out and sep:
|
|
||||||
out.append(sep)
|
|
||||||
if isinstance(item, ParseResults):
|
|
||||||
out += item._asStringList()
|
|
||||||
else:
|
|
||||||
out.append(str(item))
|
|
||||||
return out
|
|
||||||
|
|
||||||
def as_list(self) -> list:
|
|
||||||
"""
|
|
||||||
Returns the parse results as a nested list of matching tokens, all converted to strings.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
patt = Word(alphas)[1, ...]
|
|
||||||
result = patt.parse_string("sldkj lsdkj sldkj")
|
|
||||||
# even though the result prints in string-like form, it is actually a pyparsing ParseResults
|
|
||||||
print(type(result), result) # -> <class 'pyparsing.ParseResults'> ['sldkj', 'lsdkj', 'sldkj']
|
|
||||||
|
|
||||||
# Use as_list() to create an actual list
|
|
||||||
result_list = result.as_list()
|
|
||||||
print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj']
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
res.as_list() if isinstance(res, ParseResults) else res
|
|
||||||
for res in self._toklist
|
|
||||||
]
|
|
||||||
|
|
||||||
def as_dict(self) -> dict:
|
|
||||||
"""
|
|
||||||
Returns the named parse results as a nested dictionary.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
integer = Word(nums)
|
|
||||||
date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
|
|
||||||
|
|
||||||
result = date_str.parse_string('12/31/1999')
|
|
||||||
print(type(result), repr(result)) # -> <class 'pyparsing.ParseResults'> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})
|
|
||||||
|
|
||||||
result_dict = result.as_dict()
|
|
||||||
print(type(result_dict), repr(result_dict)) # -> <class 'dict'> {'day': '1999', 'year': '12', 'month': '31'}
|
|
||||||
|
|
||||||
# even though a ParseResults supports dict-like access, sometime you just need to have a dict
|
|
||||||
import json
|
|
||||||
print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable
|
|
||||||
print(json.dumps(result.as_dict())) # -> {"month": "31", "day": "1999", "year": "12"}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def to_item(obj):
|
|
||||||
if isinstance(obj, ParseResults):
|
|
||||||
return obj.as_dict() if obj.haskeys() else [to_item(v) for v in obj]
|
|
||||||
else:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
return dict((k, to_item(v)) for k, v in self.items())
|
|
||||||
|
|
||||||
def copy(self) -> "ParseResults":
|
|
||||||
"""
|
|
||||||
Returns a new copy of a :class:`ParseResults` object.
|
|
||||||
"""
|
|
||||||
ret = ParseResults(self._toklist)
|
|
||||||
ret._tokdict = self._tokdict.copy()
|
|
||||||
ret._parent = self._parent
|
|
||||||
ret._all_names |= self._all_names
|
|
||||||
ret._name = self._name
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_name(self):
|
|
||||||
r"""
|
|
||||||
Returns the results name for this token expression. Useful when several
|
|
||||||
different expressions might match at a particular location.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
integer = Word(nums)
|
|
||||||
ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d")
|
|
||||||
house_number_expr = Suppress('#') + Word(nums, alphanums)
|
|
||||||
user_data = (Group(house_number_expr)("house_number")
|
|
||||||
| Group(ssn_expr)("ssn")
|
|
||||||
| Group(integer)("age"))
|
|
||||||
user_info = user_data[1, ...]
|
|
||||||
|
|
||||||
result = user_info.parse_string("22 111-22-3333 #221B")
|
|
||||||
for item in result:
|
|
||||||
print(item.get_name(), ':', item[0])
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
age : 22
|
|
||||||
ssn : 111-22-3333
|
|
||||||
house_number : 221B
|
|
||||||
"""
|
|
||||||
if self._name:
|
|
||||||
return self._name
|
|
||||||
elif self._parent:
|
|
||||||
par = self._parent()
|
|
||||||
|
|
||||||
def find_in_parent(sub):
|
|
||||||
return next(
|
|
||||||
(
|
|
||||||
k
|
|
||||||
for k, vlist in par._tokdict.items()
|
|
||||||
for v, loc in vlist
|
|
||||||
if sub is v
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return find_in_parent(self) if par else None
|
|
||||||
elif (
|
|
||||||
len(self) == 1
|
|
||||||
and len(self._tokdict) == 1
|
|
||||||
and next(iter(self._tokdict.values()))[0][1] in (0, -1)
|
|
||||||
):
|
|
||||||
return next(iter(self._tokdict.keys()))
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def dump(self, indent="", full=True, include_list=True, _depth=0) -> str:
|
|
||||||
"""
|
|
||||||
Diagnostic method for listing out the contents of
|
|
||||||
a :class:`ParseResults`. Accepts an optional ``indent`` argument so
|
|
||||||
that this string can be embedded in a nested display of other data.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
integer = Word(nums)
|
|
||||||
date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
|
|
||||||
|
|
||||||
result = date_str.parse_string('1999/12/31')
|
|
||||||
print(result.dump())
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
['1999', '/', '12', '/', '31']
|
|
||||||
- day: '31'
|
|
||||||
- month: '12'
|
|
||||||
- year: '1999'
|
|
||||||
"""
|
|
||||||
out = []
|
|
||||||
NL = "\n"
|
|
||||||
out.append(indent + str(self.as_list()) if include_list else "")
|
|
||||||
|
|
||||||
if full:
|
|
||||||
if self.haskeys():
|
|
||||||
items = sorted((str(k), v) for k, v in self.items())
|
|
||||||
for k, v in items:
|
|
||||||
if out:
|
|
||||||
out.append(NL)
|
|
||||||
out.append("{}{}- {}: ".format(indent, (" " * _depth), k))
|
|
||||||
if isinstance(v, ParseResults):
|
|
||||||
if v:
|
|
||||||
out.append(
|
|
||||||
v.dump(
|
|
||||||
indent=indent,
|
|
||||||
full=full,
|
|
||||||
include_list=include_list,
|
|
||||||
_depth=_depth + 1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
out.append(str(v))
|
|
||||||
else:
|
|
||||||
out.append(repr(v))
|
|
||||||
if any(isinstance(vv, ParseResults) for vv in self):
|
|
||||||
v = self
|
|
||||||
for i, vv in enumerate(v):
|
|
||||||
if isinstance(vv, ParseResults):
|
|
||||||
out.append(
|
|
||||||
"\n{}{}[{}]:\n{}{}{}".format(
|
|
||||||
indent,
|
|
||||||
(" " * (_depth)),
|
|
||||||
i,
|
|
||||||
indent,
|
|
||||||
(" " * (_depth + 1)),
|
|
||||||
vv.dump(
|
|
||||||
indent=indent,
|
|
||||||
full=full,
|
|
||||||
include_list=include_list,
|
|
||||||
_depth=_depth + 1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
out.append(
|
|
||||||
"\n%s%s[%d]:\n%s%s%s"
|
|
||||||
% (
|
|
||||||
indent,
|
|
||||||
(" " * (_depth)),
|
|
||||||
i,
|
|
||||||
indent,
|
|
||||||
(" " * (_depth + 1)),
|
|
||||||
str(vv),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return "".join(out)
|
|
||||||
|
|
||||||
def pprint(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Pretty-printer for parsed results as a list, using the
|
|
||||||
`pprint <https://docs.python.org/3/library/pprint.html>`_ module.
|
|
||||||
Accepts additional positional or keyword args as defined for
|
|
||||||
`pprint.pprint <https://docs.python.org/3/library/pprint.html#pprint.pprint>`_ .
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
ident = Word(alphas, alphanums)
|
|
||||||
num = Word(nums)
|
|
||||||
func = Forward()
|
|
||||||
term = ident | num | Group('(' + func + ')')
|
|
||||||
func <<= ident + Group(Optional(delimited_list(term)))
|
|
||||||
result = func.parse_string("fna a,b,(fnb c,d,200),100")
|
|
||||||
result.pprint(width=40)
|
|
||||||
|
|
||||||
prints::
|
|
||||||
|
|
||||||
['fna',
|
|
||||||
['a',
|
|
||||||
'b',
|
|
||||||
['(', 'fnb', ['c', 'd', '200'], ')'],
|
|
||||||
'100']]
|
|
||||||
"""
|
|
||||||
pprint.pprint(self.as_list(), *args, **kwargs)
|
|
||||||
|
|
||||||
# add support for pickle protocol
|
|
||||||
def __getstate__(self):
|
|
||||||
return (
|
|
||||||
self._toklist,
|
|
||||||
(
|
|
||||||
self._tokdict.copy(),
|
|
||||||
self._parent is not None and self._parent() or None,
|
|
||||||
self._all_names,
|
|
||||||
self._name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
self._toklist, (self._tokdict, par, inAccumNames, self._name) = state
|
|
||||||
self._all_names = set(inAccumNames)
|
|
||||||
if par is not None:
|
|
||||||
self._parent = wkref(par)
|
|
||||||
else:
|
|
||||||
self._parent = None
|
|
||||||
|
|
||||||
def __getnewargs__(self):
|
|
||||||
return self._toklist, self._name
|
|
||||||
|
|
||||||
def __dir__(self):
|
|
||||||
return dir(type(self)) + list(self.keys())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, other, name=None) -> "ParseResults":
|
|
||||||
"""
|
|
||||||
Helper classmethod to construct a ``ParseResults`` from a ``dict``, preserving the
|
|
||||||
name-value relations as results names. If an optional ``name`` argument is
|
|
||||||
given, a nested ``ParseResults`` will be returned.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def is_iterable(obj):
|
|
||||||
try:
|
|
||||||
iter(obj)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return not isinstance(obj, str_type)
|
|
||||||
|
|
||||||
ret = cls([])
|
|
||||||
for k, v in other.items():
|
|
||||||
if isinstance(v, Mapping):
|
|
||||||
ret += cls.from_dict(v, name=k)
|
|
||||||
else:
|
|
||||||
ret += cls([v], name=k, asList=is_iterable(v))
|
|
||||||
if name is not None:
|
|
||||||
ret = cls([ret], name=name)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
asList = as_list
|
|
||||||
asDict = as_dict
|
|
||||||
getName = get_name
|
|
||||||
|
|
||||||
|
|
||||||
MutableMapping.register(ParseResults)
|
|
||||||
MutableSequence.register(ParseResults)
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
# testing.py
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .core import (
|
|
||||||
ParserElement,
|
|
||||||
ParseException,
|
|
||||||
Keyword,
|
|
||||||
__diag__,
|
|
||||||
__compat__,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class pyparsing_test:
|
|
||||||
"""
|
|
||||||
namespace class for classes useful in writing unit tests
|
|
||||||
"""
|
|
||||||
|
|
||||||
class reset_pyparsing_context:
|
|
||||||
"""
|
|
||||||
Context manager to be used when writing unit tests that modify pyparsing config values:
|
|
||||||
- packrat parsing
|
|
||||||
- bounded recursion parsing
|
|
||||||
- default whitespace characters.
|
|
||||||
- default keyword characters
|
|
||||||
- literal string auto-conversion class
|
|
||||||
- __diag__ settings
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
with reset_pyparsing_context():
|
|
||||||
# test that literals used to construct a grammar are automatically suppressed
|
|
||||||
ParserElement.inlineLiteralsUsing(Suppress)
|
|
||||||
|
|
||||||
term = Word(alphas) | Word(nums)
|
|
||||||
group = Group('(' + term[...] + ')')
|
|
||||||
|
|
||||||
# assert that the '()' characters are not included in the parsed tokens
|
|
||||||
self.assertParseAndCheckList(group, "(abc 123 def)", ['abc', '123', 'def'])
|
|
||||||
|
|
||||||
# after exiting context manager, literals are converted to Literal expressions again
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._save_context = {}
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS
|
|
||||||
self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS
|
|
||||||
|
|
||||||
self._save_context[
|
|
||||||
"literal_string_class"
|
|
||||||
] = ParserElement._literalStringClass
|
|
||||||
|
|
||||||
self._save_context["verbose_stacktrace"] = ParserElement.verbose_stacktrace
|
|
||||||
|
|
||||||
self._save_context["packrat_enabled"] = ParserElement._packratEnabled
|
|
||||||
if ParserElement._packratEnabled:
|
|
||||||
self._save_context[
|
|
||||||
"packrat_cache_size"
|
|
||||||
] = ParserElement.packrat_cache.size
|
|
||||||
else:
|
|
||||||
self._save_context["packrat_cache_size"] = None
|
|
||||||
self._save_context["packrat_parse"] = ParserElement._parse
|
|
||||||
self._save_context[
|
|
||||||
"recursion_enabled"
|
|
||||||
] = ParserElement._left_recursion_enabled
|
|
||||||
|
|
||||||
self._save_context["__diag__"] = {
|
|
||||||
name: getattr(__diag__, name) for name in __diag__._all_names
|
|
||||||
}
|
|
||||||
|
|
||||||
self._save_context["__compat__"] = {
|
|
||||||
"collect_all_And_tokens": __compat__.collect_all_And_tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def restore(self):
|
|
||||||
# reset pyparsing global state
|
|
||||||
if (
|
|
||||||
ParserElement.DEFAULT_WHITE_CHARS
|
|
||||||
!= self._save_context["default_whitespace"]
|
|
||||||
):
|
|
||||||
ParserElement.set_default_whitespace_chars(
|
|
||||||
self._save_context["default_whitespace"]
|
|
||||||
)
|
|
||||||
|
|
||||||
ParserElement.verbose_stacktrace = self._save_context["verbose_stacktrace"]
|
|
||||||
|
|
||||||
Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"]
|
|
||||||
ParserElement.inlineLiteralsUsing(
|
|
||||||
self._save_context["literal_string_class"]
|
|
||||||
)
|
|
||||||
|
|
||||||
for name, value in self._save_context["__diag__"].items():
|
|
||||||
(__diag__.enable if value else __diag__.disable)(name)
|
|
||||||
|
|
||||||
ParserElement._packratEnabled = False
|
|
||||||
if self._save_context["packrat_enabled"]:
|
|
||||||
ParserElement.enable_packrat(self._save_context["packrat_cache_size"])
|
|
||||||
else:
|
|
||||||
ParserElement._parse = self._save_context["packrat_parse"]
|
|
||||||
ParserElement._left_recursion_enabled = self._save_context[
|
|
||||||
"recursion_enabled"
|
|
||||||
]
|
|
||||||
|
|
||||||
__compat__.collect_all_And_tokens = self._save_context["__compat__"]
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
ret = type(self)()
|
|
||||||
ret._save_context.update(self._save_context)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self.save()
|
|
||||||
|
|
||||||
def __exit__(self, *args):
|
|
||||||
self.restore()
|
|
||||||
|
|
||||||
class TestParseResultsAsserts:
|
|
||||||
"""
|
|
||||||
A mixin class to add parse results assertion methods to normal unittest.TestCase classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def assertParseResultsEquals(
|
|
||||||
self, result, expected_list=None, expected_dict=None, msg=None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Unit test assertion to compare a :class:`ParseResults` object with an optional ``expected_list``,
|
|
||||||
and compare any defined results names with an optional ``expected_dict``.
|
|
||||||
"""
|
|
||||||
if expected_list is not None:
|
|
||||||
self.assertEqual(expected_list, result.as_list(), msg=msg)
|
|
||||||
if expected_dict is not None:
|
|
||||||
self.assertEqual(expected_dict, result.as_dict(), msg=msg)
|
|
||||||
|
|
||||||
def assertParseAndCheckList(
|
|
||||||
self, expr, test_string, expected_list, msg=None, verbose=True
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Convenience wrapper assert to test a parser element and input string, and assert that
|
|
||||||
the resulting ``ParseResults.asList()`` is equal to the ``expected_list``.
|
|
||||||
"""
|
|
||||||
result = expr.parse_string(test_string, parse_all=True)
|
|
||||||
if verbose:
|
|
||||||
print(result.dump())
|
|
||||||
else:
|
|
||||||
print(result.as_list())
|
|
||||||
self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg)
|
|
||||||
|
|
||||||
def assertParseAndCheckDict(
|
|
||||||
self, expr, test_string, expected_dict, msg=None, verbose=True
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Convenience wrapper assert to test a parser element and input string, and assert that
|
|
||||||
the resulting ``ParseResults.asDict()`` is equal to the ``expected_dict``.
|
|
||||||
"""
|
|
||||||
result = expr.parse_string(test_string, parseAll=True)
|
|
||||||
if verbose:
|
|
||||||
print(result.dump())
|
|
||||||
else:
|
|
||||||
print(result.as_list())
|
|
||||||
self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg)
|
|
||||||
|
|
||||||
def assertRunTestResults(
|
|
||||||
self, run_tests_report, expected_parse_results=None, msg=None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Unit test assertion to evaluate output of ``ParserElement.runTests()``. If a list of
|
|
||||||
list-dict tuples is given as the ``expected_parse_results`` argument, then these are zipped
|
|
||||||
with the report tuples returned by ``runTests`` and evaluated using ``assertParseResultsEquals``.
|
|
||||||
Finally, asserts that the overall ``runTests()`` success value is ``True``.
|
|
||||||
|
|
||||||
:param run_tests_report: tuple(bool, [tuple(str, ParseResults or Exception)]) returned from runTests
|
|
||||||
:param expected_parse_results (optional): [tuple(str, list, dict, Exception)]
|
|
||||||
"""
|
|
||||||
run_test_success, run_test_results = run_tests_report
|
|
||||||
|
|
||||||
if expected_parse_results is not None:
|
|
||||||
merged = [
|
|
||||||
(*rpt, expected)
|
|
||||||
for rpt, expected in zip(run_test_results, expected_parse_results)
|
|
||||||
]
|
|
||||||
for test_string, result, expected in merged:
|
|
||||||
# expected should be a tuple containing a list and/or a dict or an exception,
|
|
||||||
# and optional failure message string
|
|
||||||
# an empty tuple will skip any result validation
|
|
||||||
fail_msg = next(
|
|
||||||
(exp for exp in expected if isinstance(exp, str)), None
|
|
||||||
)
|
|
||||||
expected_exception = next(
|
|
||||||
(
|
|
||||||
exp
|
|
||||||
for exp in expected
|
|
||||||
if isinstance(exp, type) and issubclass(exp, Exception)
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if expected_exception is not None:
|
|
||||||
with self.assertRaises(
|
|
||||||
expected_exception=expected_exception, msg=fail_msg or msg
|
|
||||||
):
|
|
||||||
if isinstance(result, Exception):
|
|
||||||
raise result
|
|
||||||
else:
|
|
||||||
expected_list = next(
|
|
||||||
(exp for exp in expected if isinstance(exp, list)), None
|
|
||||||
)
|
|
||||||
expected_dict = next(
|
|
||||||
(exp for exp in expected if isinstance(exp, dict)), None
|
|
||||||
)
|
|
||||||
if (expected_list, expected_dict) != (None, None):
|
|
||||||
self.assertParseResultsEquals(
|
|
||||||
result,
|
|
||||||
expected_list=expected_list,
|
|
||||||
expected_dict=expected_dict,
|
|
||||||
msg=fail_msg or msg,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# warning here maybe?
|
|
||||||
print("no validation for {!r}".format(test_string))
|
|
||||||
|
|
||||||
# do this last, in case some specific test results can be reported instead
|
|
||||||
self.assertTrue(
|
|
||||||
run_test_success, msg=msg if msg is not None else "failed runTests"
|
|
||||||
)
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def assertRaisesParseException(self, exc_type=ParseException, msg=None):
|
|
||||||
with self.assertRaises(exc_type, msg=msg):
|
|
||||||
yield
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def with_line_numbers(
|
|
||||||
s: str,
|
|
||||||
start_line: typing.Optional[int] = None,
|
|
||||||
end_line: typing.Optional[int] = None,
|
|
||||||
expand_tabs: bool = True,
|
|
||||||
eol_mark: str = "|",
|
|
||||||
mark_spaces: typing.Optional[str] = None,
|
|
||||||
mark_control: typing.Optional[str] = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Helpful method for debugging a parser - prints a string with line and column numbers.
|
|
||||||
(Line and column numbers are 1-based.)
|
|
||||||
|
|
||||||
:param s: tuple(bool, str - string to be printed with line and column numbers
|
|
||||||
:param start_line: int - (optional) starting line number in s to print (default=1)
|
|
||||||
:param end_line: int - (optional) ending line number in s to print (default=len(s))
|
|
||||||
:param expand_tabs: bool - (optional) expand tabs to spaces, to match the pyparsing default
|
|
||||||
:param eol_mark: str - (optional) string to mark the end of lines, helps visualize trailing spaces (default="|")
|
|
||||||
:param mark_spaces: str - (optional) special character to display in place of spaces
|
|
||||||
:param mark_control: str - (optional) convert non-printing control characters to a placeholding
|
|
||||||
character; valid values:
|
|
||||||
- "unicode" - replaces control chars with Unicode symbols, such as "␍" and "␊"
|
|
||||||
- any single character string - replace control characters with given string
|
|
||||||
- None (default) - string is displayed as-is
|
|
||||||
|
|
||||||
:return: str - input string with leading line numbers and column number headers
|
|
||||||
"""
|
|
||||||
if expand_tabs:
|
|
||||||
s = s.expandtabs()
|
|
||||||
if mark_control is not None:
|
|
||||||
if mark_control == "unicode":
|
|
||||||
tbl = str.maketrans(
|
|
||||||
{c: u for c, u in zip(range(0, 33), range(0x2400, 0x2433))}
|
|
||||||
| {127: 0x2421}
|
|
||||||
)
|
|
||||||
eol_mark = ""
|
|
||||||
else:
|
|
||||||
tbl = str.maketrans(
|
|
||||||
{c: mark_control for c in list(range(0, 32)) + [127]}
|
|
||||||
)
|
|
||||||
s = s.translate(tbl)
|
|
||||||
if mark_spaces is not None and mark_spaces != " ":
|
|
||||||
if mark_spaces == "unicode":
|
|
||||||
tbl = str.maketrans({9: 0x2409, 32: 0x2423})
|
|
||||||
s = s.translate(tbl)
|
|
||||||
else:
|
|
||||||
s = s.replace(" ", mark_spaces)
|
|
||||||
if start_line is None:
|
|
||||||
start_line = 1
|
|
||||||
if end_line is None:
|
|
||||||
end_line = len(s)
|
|
||||||
end_line = min(end_line, len(s))
|
|
||||||
start_line = min(max(1, start_line), end_line)
|
|
||||||
|
|
||||||
if mark_control != "unicode":
|
|
||||||
s_lines = s.splitlines()[start_line - 1 : end_line]
|
|
||||||
else:
|
|
||||||
s_lines = [line + "␊" for line in s.split("␊")[start_line - 1 : end_line]]
|
|
||||||
if not s_lines:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
lineno_width = len(str(end_line))
|
|
||||||
max_line_len = max(len(line) for line in s_lines)
|
|
||||||
lead = " " * (lineno_width + 1)
|
|
||||||
if max_line_len >= 99:
|
|
||||||
header0 = (
|
|
||||||
lead
|
|
||||||
+ "".join(
|
|
||||||
"{}{}".format(" " * 99, (i + 1) % 100)
|
|
||||||
for i in range(max(max_line_len // 100, 1))
|
|
||||||
)
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
header0 = ""
|
|
||||||
header1 = (
|
|
||||||
header0
|
|
||||||
+ lead
|
|
||||||
+ "".join(
|
|
||||||
" {}".format((i + 1) % 10)
|
|
||||||
for i in range(-(-max_line_len // 10))
|
|
||||||
)
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
header2 = lead + "1234567890" * (-(-max_line_len // 10)) + "\n"
|
|
||||||
return (
|
|
||||||
header1
|
|
||||||
+ header2
|
|
||||||
+ "\n".join(
|
|
||||||
"{:{}d}:{}{}".format(i, lineno_width, line, eol_mark)
|
|
||||||
for i, line in enumerate(s_lines, start=start_line)
|
|
||||||
)
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
# unicode.py
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from itertools import filterfalse
|
|
||||||
from typing import List, Tuple, Union
|
|
||||||
|
|
||||||
|
|
||||||
class _lazyclassproperty:
|
|
||||||
def __init__(self, fn):
|
|
||||||
self.fn = fn
|
|
||||||
self.__doc__ = fn.__doc__
|
|
||||||
self.__name__ = fn.__name__
|
|
||||||
|
|
||||||
def __get__(self, obj, cls):
|
|
||||||
if cls is None:
|
|
||||||
cls = type(obj)
|
|
||||||
if not hasattr(cls, "_intern") or any(
|
|
||||||
cls._intern is getattr(superclass, "_intern", [])
|
|
||||||
for superclass in cls.__mro__[1:]
|
|
||||||
):
|
|
||||||
cls._intern = {}
|
|
||||||
attrname = self.fn.__name__
|
|
||||||
if attrname not in cls._intern:
|
|
||||||
cls._intern[attrname] = self.fn(cls)
|
|
||||||
return cls._intern[attrname]
|
|
||||||
|
|
||||||
|
|
||||||
UnicodeRangeList = List[Union[Tuple[int, int], Tuple[int]]]
|
|
||||||
|
|
||||||
|
|
||||||
class unicode_set:
|
|
||||||
"""
|
|
||||||
A set of Unicode characters, for language-specific strings for
|
|
||||||
``alphas``, ``nums``, ``alphanums``, and ``printables``.
|
|
||||||
A unicode_set is defined by a list of ranges in the Unicode character
|
|
||||||
set, in a class attribute ``_ranges``. Ranges can be specified using
|
|
||||||
2-tuples or a 1-tuple, such as::
|
|
||||||
|
|
||||||
_ranges = [
|
|
||||||
(0x0020, 0x007e),
|
|
||||||
(0x00a0, 0x00ff),
|
|
||||||
(0x0100,),
|
|
||||||
]
|
|
||||||
|
|
||||||
Ranges are left- and right-inclusive. A 1-tuple of (x,) is treated as (x, x).
|
|
||||||
|
|
||||||
A unicode set can also be defined using multiple inheritance of other unicode sets::
|
|
||||||
|
|
||||||
class CJK(Chinese, Japanese, Korean):
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ranges: UnicodeRangeList = []
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def _chars_for_ranges(cls):
|
|
||||||
ret = []
|
|
||||||
for cc in cls.__mro__:
|
|
||||||
if cc is unicode_set:
|
|
||||||
break
|
|
||||||
for rr in getattr(cc, "_ranges", ()):
|
|
||||||
ret.extend(range(rr[0], rr[-1] + 1))
|
|
||||||
return [chr(c) for c in sorted(set(ret))]
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def printables(cls):
|
|
||||||
"all non-whitespace characters in this range"
|
|
||||||
return "".join(filterfalse(str.isspace, cls._chars_for_ranges))
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def alphas(cls):
|
|
||||||
"all alphabetic characters in this range"
|
|
||||||
return "".join(filter(str.isalpha, cls._chars_for_ranges))
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def nums(cls):
|
|
||||||
"all numeric digit characters in this range"
|
|
||||||
return "".join(filter(str.isdigit, cls._chars_for_ranges))
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def alphanums(cls):
|
|
||||||
"all alphanumeric characters in this range"
|
|
||||||
return cls.alphas + cls.nums
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def identchars(cls):
|
|
||||||
"all characters in this range that are valid identifier characters, plus underscore '_'"
|
|
||||||
return "".join(
|
|
||||||
sorted(
|
|
||||||
set(
|
|
||||||
"".join(filter(str.isidentifier, cls._chars_for_ranges))
|
|
||||||
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzªµº"
|
|
||||||
+ "ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
|
|
||||||
+ "_"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@_lazyclassproperty
|
|
||||||
def identbodychars(cls):
|
|
||||||
"""
|
|
||||||
all characters in this range that are valid identifier body characters,
|
|
||||||
plus the digits 0-9
|
|
||||||
"""
|
|
||||||
return "".join(
|
|
||||||
sorted(
|
|
||||||
set(
|
|
||||||
cls.identchars
|
|
||||||
+ "0123456789"
|
|
||||||
+ "".join(
|
|
||||||
[c for c in cls._chars_for_ranges if ("_" + c).isidentifier()]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class pyparsing_unicode(unicode_set):
|
|
||||||
"""
|
|
||||||
A namespace class for defining common language unicode_sets.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# fmt: off
|
|
||||||
|
|
||||||
# define ranges in language character sets
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0020, sys.maxunicode),
|
|
||||||
]
|
|
||||||
|
|
||||||
class BasicMultilingualPlane(unicode_set):
|
|
||||||
"Unicode set for the Basic Multilingual Plane"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0020, 0xFFFF),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Latin1(unicode_set):
|
|
||||||
"Unicode set for Latin-1 Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0020, 0x007E),
|
|
||||||
(0x00A0, 0x00FF),
|
|
||||||
]
|
|
||||||
|
|
||||||
class LatinA(unicode_set):
|
|
||||||
"Unicode set for Latin-A Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0100, 0x017F),
|
|
||||||
]
|
|
||||||
|
|
||||||
class LatinB(unicode_set):
|
|
||||||
"Unicode set for Latin-B Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0180, 0x024F),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Greek(unicode_set):
|
|
||||||
"Unicode set for Greek Unicode Character Ranges"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0342, 0x0345),
|
|
||||||
(0x0370, 0x0377),
|
|
||||||
(0x037A, 0x037F),
|
|
||||||
(0x0384, 0x038A),
|
|
||||||
(0x038C,),
|
|
||||||
(0x038E, 0x03A1),
|
|
||||||
(0x03A3, 0x03E1),
|
|
||||||
(0x03F0, 0x03FF),
|
|
||||||
(0x1D26, 0x1D2A),
|
|
||||||
(0x1D5E,),
|
|
||||||
(0x1D60,),
|
|
||||||
(0x1D66, 0x1D6A),
|
|
||||||
(0x1F00, 0x1F15),
|
|
||||||
(0x1F18, 0x1F1D),
|
|
||||||
(0x1F20, 0x1F45),
|
|
||||||
(0x1F48, 0x1F4D),
|
|
||||||
(0x1F50, 0x1F57),
|
|
||||||
(0x1F59,),
|
|
||||||
(0x1F5B,),
|
|
||||||
(0x1F5D,),
|
|
||||||
(0x1F5F, 0x1F7D),
|
|
||||||
(0x1F80, 0x1FB4),
|
|
||||||
(0x1FB6, 0x1FC4),
|
|
||||||
(0x1FC6, 0x1FD3),
|
|
||||||
(0x1FD6, 0x1FDB),
|
|
||||||
(0x1FDD, 0x1FEF),
|
|
||||||
(0x1FF2, 0x1FF4),
|
|
||||||
(0x1FF6, 0x1FFE),
|
|
||||||
(0x2129,),
|
|
||||||
(0x2719, 0x271A),
|
|
||||||
(0xAB65,),
|
|
||||||
(0x10140, 0x1018D),
|
|
||||||
(0x101A0,),
|
|
||||||
(0x1D200, 0x1D245),
|
|
||||||
(0x1F7A1, 0x1F7A7),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Cyrillic(unicode_set):
|
|
||||||
"Unicode set for Cyrillic Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0400, 0x052F),
|
|
||||||
(0x1C80, 0x1C88),
|
|
||||||
(0x1D2B,),
|
|
||||||
(0x1D78,),
|
|
||||||
(0x2DE0, 0x2DFF),
|
|
||||||
(0xA640, 0xA672),
|
|
||||||
(0xA674, 0xA69F),
|
|
||||||
(0xFE2E, 0xFE2F),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Chinese(unicode_set):
|
|
||||||
"Unicode set for Chinese Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x2E80, 0x2E99),
|
|
||||||
(0x2E9B, 0x2EF3),
|
|
||||||
(0x31C0, 0x31E3),
|
|
||||||
(0x3400, 0x4DB5),
|
|
||||||
(0x4E00, 0x9FEF),
|
|
||||||
(0xA700, 0xA707),
|
|
||||||
(0xF900, 0xFA6D),
|
|
||||||
(0xFA70, 0xFAD9),
|
|
||||||
(0x16FE2, 0x16FE3),
|
|
||||||
(0x1F210, 0x1F212),
|
|
||||||
(0x1F214, 0x1F23B),
|
|
||||||
(0x1F240, 0x1F248),
|
|
||||||
(0x20000, 0x2A6D6),
|
|
||||||
(0x2A700, 0x2B734),
|
|
||||||
(0x2B740, 0x2B81D),
|
|
||||||
(0x2B820, 0x2CEA1),
|
|
||||||
(0x2CEB0, 0x2EBE0),
|
|
||||||
(0x2F800, 0x2FA1D),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Japanese(unicode_set):
|
|
||||||
"Unicode set for Japanese Unicode Character Range, combining Kanji, Hiragana, and Katakana ranges"
|
|
||||||
_ranges: UnicodeRangeList = []
|
|
||||||
|
|
||||||
class Kanji(unicode_set):
|
|
||||||
"Unicode set for Kanji Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x4E00, 0x9FBF),
|
|
||||||
(0x3000, 0x303F),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Hiragana(unicode_set):
|
|
||||||
"Unicode set for Hiragana Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x3041, 0x3096),
|
|
||||||
(0x3099, 0x30A0),
|
|
||||||
(0x30FC,),
|
|
||||||
(0xFF70,),
|
|
||||||
(0x1B001,),
|
|
||||||
(0x1B150, 0x1B152),
|
|
||||||
(0x1F200,),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Katakana(unicode_set):
|
|
||||||
"Unicode set for Katakana Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x3099, 0x309C),
|
|
||||||
(0x30A0, 0x30FF),
|
|
||||||
(0x31F0, 0x31FF),
|
|
||||||
(0x32D0, 0x32FE),
|
|
||||||
(0xFF65, 0xFF9F),
|
|
||||||
(0x1B000,),
|
|
||||||
(0x1B164, 0x1B167),
|
|
||||||
(0x1F201, 0x1F202),
|
|
||||||
(0x1F213,),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Hangul(unicode_set):
|
|
||||||
"Unicode set for Hangul (Korean) Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x1100, 0x11FF),
|
|
||||||
(0x302E, 0x302F),
|
|
||||||
(0x3131, 0x318E),
|
|
||||||
(0x3200, 0x321C),
|
|
||||||
(0x3260, 0x327B),
|
|
||||||
(0x327E,),
|
|
||||||
(0xA960, 0xA97C),
|
|
||||||
(0xAC00, 0xD7A3),
|
|
||||||
(0xD7B0, 0xD7C6),
|
|
||||||
(0xD7CB, 0xD7FB),
|
|
||||||
(0xFFA0, 0xFFBE),
|
|
||||||
(0xFFC2, 0xFFC7),
|
|
||||||
(0xFFCA, 0xFFCF),
|
|
||||||
(0xFFD2, 0xFFD7),
|
|
||||||
(0xFFDA, 0xFFDC),
|
|
||||||
]
|
|
||||||
|
|
||||||
Korean = Hangul
|
|
||||||
|
|
||||||
class CJK(Chinese, Japanese, Hangul):
|
|
||||||
"Unicode set for combined Chinese, Japanese, and Korean (CJK) Unicode Character Range"
|
|
||||||
|
|
||||||
class Thai(unicode_set):
|
|
||||||
"Unicode set for Thai Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0E01, 0x0E3A),
|
|
||||||
(0x0E3F, 0x0E5B)
|
|
||||||
]
|
|
||||||
|
|
||||||
class Arabic(unicode_set):
|
|
||||||
"Unicode set for Arabic Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0600, 0x061B),
|
|
||||||
(0x061E, 0x06FF),
|
|
||||||
(0x0700, 0x077F),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Hebrew(unicode_set):
|
|
||||||
"Unicode set for Hebrew Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0591, 0x05C7),
|
|
||||||
(0x05D0, 0x05EA),
|
|
||||||
(0x05EF, 0x05F4),
|
|
||||||
(0xFB1D, 0xFB36),
|
|
||||||
(0xFB38, 0xFB3C),
|
|
||||||
(0xFB3E,),
|
|
||||||
(0xFB40, 0xFB41),
|
|
||||||
(0xFB43, 0xFB44),
|
|
||||||
(0xFB46, 0xFB4F),
|
|
||||||
]
|
|
||||||
|
|
||||||
class Devanagari(unicode_set):
|
|
||||||
"Unicode set for Devanagari Unicode Character Range"
|
|
||||||
_ranges: UnicodeRangeList = [
|
|
||||||
(0x0900, 0x097F),
|
|
||||||
(0xA8E0, 0xA8FF)
|
|
||||||
]
|
|
||||||
|
|
||||||
# fmt: on
|
|
||||||
|
|
||||||
|
|
||||||
pyparsing_unicode.Japanese._ranges = (
|
|
||||||
pyparsing_unicode.Japanese.Kanji._ranges
|
|
||||||
+ pyparsing_unicode.Japanese.Hiragana._ranges
|
|
||||||
+ pyparsing_unicode.Japanese.Katakana._ranges
|
|
||||||
)
|
|
||||||
|
|
||||||
pyparsing_unicode.BMP = pyparsing_unicode.BasicMultilingualPlane
|
|
||||||
|
|
||||||
# add language identifiers using language Unicode
|
|
||||||
pyparsing_unicode.العربية = pyparsing_unicode.Arabic
|
|
||||||
pyparsing_unicode.中文 = pyparsing_unicode.Chinese
|
|
||||||
pyparsing_unicode.кириллица = pyparsing_unicode.Cyrillic
|
|
||||||
pyparsing_unicode.Ελληνικά = pyparsing_unicode.Greek
|
|
||||||
pyparsing_unicode.עִברִית = pyparsing_unicode.Hebrew
|
|
||||||
pyparsing_unicode.日本語 = pyparsing_unicode.Japanese
|
|
||||||
pyparsing_unicode.Japanese.漢字 = pyparsing_unicode.Japanese.Kanji
|
|
||||||
pyparsing_unicode.Japanese.カタカナ = pyparsing_unicode.Japanese.Katakana
|
|
||||||
pyparsing_unicode.Japanese.ひらがな = pyparsing_unicode.Japanese.Hiragana
|
|
||||||
pyparsing_unicode.한국어 = pyparsing_unicode.Korean
|
|
||||||
pyparsing_unicode.ไทย = pyparsing_unicode.Thai
|
|
||||||
pyparsing_unicode.देवनागरी = pyparsing_unicode.Devanagari
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
# util.py
|
|
||||||
import warnings
|
|
||||||
import types
|
|
||||||
import collections
|
|
||||||
import itertools
|
|
||||||
from functools import lru_cache
|
|
||||||
from typing import List, Union, Iterable
|
|
||||||
|
|
||||||
_bslash = chr(92)
|
|
||||||
|
|
||||||
|
|
||||||
class __config_flags:
|
|
||||||
"""Internal class for defining compatibility and debugging flags"""
|
|
||||||
|
|
||||||
_all_names: List[str] = []
|
|
||||||
_fixed_names: List[str] = []
|
|
||||||
_type_desc = "configuration"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _set(cls, dname, value):
|
|
||||||
if dname in cls._fixed_names:
|
|
||||||
warnings.warn(
|
|
||||||
"{}.{} {} is {} and cannot be overridden".format(
|
|
||||||
cls.__name__,
|
|
||||||
dname,
|
|
||||||
cls._type_desc,
|
|
||||||
str(getattr(cls, dname)).upper(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if dname in cls._all_names:
|
|
||||||
setattr(cls, dname, value)
|
|
||||||
else:
|
|
||||||
raise ValueError("no such {} {!r}".format(cls._type_desc, dname))
|
|
||||||
|
|
||||||
enable = classmethod(lambda cls, name: cls._set(name, True))
|
|
||||||
disable = classmethod(lambda cls, name: cls._set(name, False))
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
|
||||||
def col(loc: int, strg: str) -> int:
|
|
||||||
"""
|
|
||||||
Returns current column within a string, counting newlines as line separators.
|
|
||||||
The first column is number 1.
|
|
||||||
|
|
||||||
Note: the default parsing behavior is to expand tabs in the input string
|
|
||||||
before starting the parsing process. See
|
|
||||||
:class:`ParserElement.parseString` for more
|
|
||||||
information on parsing strings containing ``<TAB>`` s, and suggested
|
|
||||||
methods to maintain a consistent view of the parsed string, the parse
|
|
||||||
location, and line and column positions within the parsed string.
|
|
||||||
"""
|
|
||||||
s = strg
|
|
||||||
return 1 if 0 < loc < len(s) and s[loc - 1] == "\n" else loc - s.rfind("\n", 0, loc)
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
|
||||||
def lineno(loc: int, strg: str) -> int:
|
|
||||||
"""Returns current line number within a string, counting newlines as line separators.
|
|
||||||
The first line is number 1.
|
|
||||||
|
|
||||||
Note - the default parsing behavior is to expand tabs in the input string
|
|
||||||
before starting the parsing process. See :class:`ParserElement.parseString`
|
|
||||||
for more information on parsing strings containing ``<TAB>`` s, and
|
|
||||||
suggested methods to maintain a consistent view of the parsed string, the
|
|
||||||
parse location, and line and column positions within the parsed string.
|
|
||||||
"""
|
|
||||||
return strg.count("\n", 0, loc) + 1
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=128)
|
|
||||||
def line(loc: int, strg: str) -> str:
|
|
||||||
"""
|
|
||||||
Returns the line of text containing loc within a string, counting newlines as line separators.
|
|
||||||
"""
|
|
||||||
last_cr = strg.rfind("\n", 0, loc)
|
|
||||||
next_cr = strg.find("\n", loc)
|
|
||||||
return strg[last_cr + 1 : next_cr] if next_cr >= 0 else strg[last_cr + 1 :]
|
|
||||||
|
|
||||||
|
|
||||||
class _UnboundedCache:
|
|
||||||
def __init__(self):
|
|
||||||
cache = {}
|
|
||||||
cache_get = cache.get
|
|
||||||
self.not_in_cache = not_in_cache = object()
|
|
||||||
|
|
||||||
def get(_, key):
|
|
||||||
return cache_get(key, not_in_cache)
|
|
||||||
|
|
||||||
def set_(_, key, value):
|
|
||||||
cache[key] = value
|
|
||||||
|
|
||||||
def clear(_):
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
self.size = None
|
|
||||||
self.get = types.MethodType(get, self)
|
|
||||||
self.set = types.MethodType(set_, self)
|
|
||||||
self.clear = types.MethodType(clear, self)
|
|
||||||
|
|
||||||
|
|
||||||
class _FifoCache:
|
|
||||||
def __init__(self, size):
|
|
||||||
self.not_in_cache = not_in_cache = object()
|
|
||||||
cache = collections.OrderedDict()
|
|
||||||
cache_get = cache.get
|
|
||||||
|
|
||||||
def get(_, key):
|
|
||||||
return cache_get(key, not_in_cache)
|
|
||||||
|
|
||||||
def set_(_, key, value):
|
|
||||||
cache[key] = value
|
|
||||||
while len(cache) > size:
|
|
||||||
cache.popitem(last=False)
|
|
||||||
|
|
||||||
def clear(_):
|
|
||||||
cache.clear()
|
|
||||||
|
|
||||||
self.size = size
|
|
||||||
self.get = types.MethodType(get, self)
|
|
||||||
self.set = types.MethodType(set_, self)
|
|
||||||
self.clear = types.MethodType(clear, self)
|
|
||||||
|
|
||||||
|
|
||||||
class LRUMemo:
|
|
||||||
"""
|
|
||||||
A memoizing mapping that retains `capacity` deleted items
|
|
||||||
|
|
||||||
The memo tracks retained items by their access order; once `capacity` items
|
|
||||||
are retained, the least recently used item is discarded.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, capacity):
|
|
||||||
self._capacity = capacity
|
|
||||||
self._active = {}
|
|
||||||
self._memory = collections.OrderedDict()
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
try:
|
|
||||||
return self._active[key]
|
|
||||||
except KeyError:
|
|
||||||
self._memory.move_to_end(key)
|
|
||||||
return self._memory[key]
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self._memory.pop(key, None)
|
|
||||||
self._active[key] = value
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
try:
|
|
||||||
value = self._active.pop(key)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
while len(self._memory) >= self._capacity:
|
|
||||||
self._memory.popitem(last=False)
|
|
||||||
self._memory[key] = value
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self._active.clear()
|
|
||||||
self._memory.clear()
|
|
||||||
|
|
||||||
|
|
||||||
class UnboundedMemo(dict):
|
|
||||||
"""
|
|
||||||
A memoizing mapping that retains all deleted items
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_regex_range_chars(s: str) -> str:
|
|
||||||
# escape these chars: ^-[]
|
|
||||||
for c in r"\^-[]":
|
|
||||||
s = s.replace(c, _bslash + c)
|
|
||||||
s = s.replace("\n", r"\n")
|
|
||||||
s = s.replace("\t", r"\t")
|
|
||||||
return str(s)
|
|
||||||
|
|
||||||
|
|
||||||
def _collapse_string_to_ranges(
|
|
||||||
s: Union[str, Iterable[str]], re_escape: bool = True
|
|
||||||
) -> str:
|
|
||||||
def is_consecutive(c):
|
|
||||||
c_int = ord(c)
|
|
||||||
is_consecutive.prev, prev = c_int, is_consecutive.prev
|
|
||||||
if c_int - prev > 1:
|
|
||||||
is_consecutive.value = next(is_consecutive.counter)
|
|
||||||
return is_consecutive.value
|
|
||||||
|
|
||||||
is_consecutive.prev = 0
|
|
||||||
is_consecutive.counter = itertools.count()
|
|
||||||
is_consecutive.value = -1
|
|
||||||
|
|
||||||
def escape_re_range_char(c):
|
|
||||||
return "\\" + c if c in r"\^-][" else c
|
|
||||||
|
|
||||||
def no_escape_re_range_char(c):
|
|
||||||
return c
|
|
||||||
|
|
||||||
if not re_escape:
|
|
||||||
escape_re_range_char = no_escape_re_range_char
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
s = "".join(sorted(set(s)))
|
|
||||||
if len(s) > 3:
|
|
||||||
for _, chars in itertools.groupby(s, key=is_consecutive):
|
|
||||||
first = last = next(chars)
|
|
||||||
last = collections.deque(
|
|
||||||
itertools.chain(iter([last]), chars), maxlen=1
|
|
||||||
).pop()
|
|
||||||
if first == last:
|
|
||||||
ret.append(escape_re_range_char(first))
|
|
||||||
else:
|
|
||||||
sep = "" if ord(last) == ord(first) + 1 else "-"
|
|
||||||
ret.append(
|
|
||||||
"{}{}{}".format(
|
|
||||||
escape_re_range_char(first), sep, escape_re_range_char(last)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ret = [escape_re_range_char(c) for c in s]
|
|
||||||
|
|
||||||
return "".join(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def _flatten(ll: list) -> list:
|
|
||||||
ret = []
|
|
||||||
for i in ll:
|
|
||||||
if isinstance(i, list):
|
|
||||||
ret.extend(_flatten(i))
|
|
||||||
else:
|
|
||||||
ret.append(i)
|
|
||||||
return ret
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
|
||||||
# Licensed to PSF under a Contributor Agreement.
|
|
||||||
|
|
||||||
__all__ = ("loads", "load", "TOMLDecodeError")
|
|
||||||
__version__ = "2.0.1" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
|
|
||||||
|
|
||||||
from ._parser import TOMLDecodeError, load, loads
|
|
||||||
|
|
||||||
# Pretend this exception was created here.
|
|
||||||
TOMLDecodeError.__module__ = __name__
|
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,691 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
|
||||||
# Licensed to PSF under a Contributor Agreement.
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Iterable
|
|
||||||
import string
|
|
||||||
from types import MappingProxyType
|
|
||||||
from typing import Any, BinaryIO, NamedTuple
|
|
||||||
|
|
||||||
from ._re import (
|
|
||||||
RE_DATETIME,
|
|
||||||
RE_LOCALTIME,
|
|
||||||
RE_NUMBER,
|
|
||||||
match_to_datetime,
|
|
||||||
match_to_localtime,
|
|
||||||
match_to_number,
|
|
||||||
)
|
|
||||||
from ._types import Key, ParseFloat, Pos
|
|
||||||
|
|
||||||
ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
|
|
||||||
|
|
||||||
# Neither of these sets include quotation mark or backslash. They are
|
|
||||||
# currently handled as separate cases in the parser functions.
|
|
||||||
ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t")
|
|
||||||
ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n")
|
|
||||||
|
|
||||||
ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS
|
|
||||||
ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
|
||||||
|
|
||||||
ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS
|
|
||||||
|
|
||||||
TOML_WS = frozenset(" \t")
|
|
||||||
TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n")
|
|
||||||
BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_")
|
|
||||||
KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'")
|
|
||||||
HEXDIGIT_CHARS = frozenset(string.hexdigits)
|
|
||||||
|
|
||||||
BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType(
|
|
||||||
{
|
|
||||||
"\\b": "\u0008", # backspace
|
|
||||||
"\\t": "\u0009", # tab
|
|
||||||
"\\n": "\u000A", # linefeed
|
|
||||||
"\\f": "\u000C", # form feed
|
|
||||||
"\\r": "\u000D", # carriage return
|
|
||||||
'\\"': "\u0022", # quote
|
|
||||||
"\\\\": "\u005C", # backslash
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TOMLDecodeError(ValueError):
|
|
||||||
"""An error raised if a document is not valid TOML."""
|
|
||||||
|
|
||||||
|
|
||||||
def load(__fp: BinaryIO, *, parse_float: ParseFloat = float) -> dict[str, Any]:
|
|
||||||
"""Parse TOML from a binary file object."""
|
|
||||||
b = __fp.read()
|
|
||||||
try:
|
|
||||||
s = b.decode()
|
|
||||||
except AttributeError:
|
|
||||||
raise TypeError(
|
|
||||||
"File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
|
|
||||||
) from None
|
|
||||||
return loads(s, parse_float=parse_float)
|
|
||||||
|
|
||||||
|
|
||||||
def loads(__s: str, *, parse_float: ParseFloat = float) -> dict[str, Any]: # noqa: C901
|
|
||||||
"""Parse TOML from a string."""
|
|
||||||
|
|
||||||
# The spec allows converting "\r\n" to "\n", even in string
|
|
||||||
# literals. Let's do so to simplify parsing.
|
|
||||||
src = __s.replace("\r\n", "\n")
|
|
||||||
pos = 0
|
|
||||||
out = Output(NestedDict(), Flags())
|
|
||||||
header: Key = ()
|
|
||||||
parse_float = make_safe_parse_float(parse_float)
|
|
||||||
|
|
||||||
# Parse one statement at a time
|
|
||||||
# (typically means one line in TOML source)
|
|
||||||
while True:
|
|
||||||
# 1. Skip line leading whitespace
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
|
|
||||||
# 2. Parse rules. Expect one of the following:
|
|
||||||
# - end of file
|
|
||||||
# - end of line
|
|
||||||
# - comment
|
|
||||||
# - key/value pair
|
|
||||||
# - append dict to list (and move to its namespace)
|
|
||||||
# - create dict (and move to its namespace)
|
|
||||||
# Skip trailing whitespace when applicable.
|
|
||||||
try:
|
|
||||||
char = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
break
|
|
||||||
if char == "\n":
|
|
||||||
pos += 1
|
|
||||||
continue
|
|
||||||
if char in KEY_INITIAL_CHARS:
|
|
||||||
pos = key_value_rule(src, pos, out, header, parse_float)
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
elif char == "[":
|
|
||||||
try:
|
|
||||||
second_char: str | None = src[pos + 1]
|
|
||||||
except IndexError:
|
|
||||||
second_char = None
|
|
||||||
out.flags.finalize_pending()
|
|
||||||
if second_char == "[":
|
|
||||||
pos, header = create_list_rule(src, pos, out)
|
|
||||||
else:
|
|
||||||
pos, header = create_dict_rule(src, pos, out)
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
elif char != "#":
|
|
||||||
raise suffixed_err(src, pos, "Invalid statement")
|
|
||||||
|
|
||||||
# 3. Skip comment
|
|
||||||
pos = skip_comment(src, pos)
|
|
||||||
|
|
||||||
# 4. Expect end of line or end of file
|
|
||||||
try:
|
|
||||||
char = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
break
|
|
||||||
if char != "\n":
|
|
||||||
raise suffixed_err(
|
|
||||||
src, pos, "Expected newline or end of document after a statement"
|
|
||||||
)
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
return out.data.dict
|
|
||||||
|
|
||||||
|
|
||||||
class Flags:
|
|
||||||
"""Flags that map to parsed keys/namespaces."""
|
|
||||||
|
|
||||||
# Marks an immutable namespace (inline array or inline table).
|
|
||||||
FROZEN = 0
|
|
||||||
# Marks a nest that has been explicitly created and can no longer
|
|
||||||
# be opened using the "[table]" syntax.
|
|
||||||
EXPLICIT_NEST = 1
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._flags: dict[str, dict] = {}
|
|
||||||
self._pending_flags: set[tuple[Key, int]] = set()
|
|
||||||
|
|
||||||
def add_pending(self, key: Key, flag: int) -> None:
|
|
||||||
self._pending_flags.add((key, flag))
|
|
||||||
|
|
||||||
def finalize_pending(self) -> None:
|
|
||||||
for key, flag in self._pending_flags:
|
|
||||||
self.set(key, flag, recursive=False)
|
|
||||||
self._pending_flags.clear()
|
|
||||||
|
|
||||||
def unset_all(self, key: Key) -> None:
|
|
||||||
cont = self._flags
|
|
||||||
for k in key[:-1]:
|
|
||||||
if k not in cont:
|
|
||||||
return
|
|
||||||
cont = cont[k]["nested"]
|
|
||||||
cont.pop(key[-1], None)
|
|
||||||
|
|
||||||
def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003
|
|
||||||
cont = self._flags
|
|
||||||
key_parent, key_stem = key[:-1], key[-1]
|
|
||||||
for k in key_parent:
|
|
||||||
if k not in cont:
|
|
||||||
cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
|
|
||||||
cont = cont[k]["nested"]
|
|
||||||
if key_stem not in cont:
|
|
||||||
cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}}
|
|
||||||
cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag)
|
|
||||||
|
|
||||||
def is_(self, key: Key, flag: int) -> bool:
|
|
||||||
if not key:
|
|
||||||
return False # document root has no flags
|
|
||||||
cont = self._flags
|
|
||||||
for k in key[:-1]:
|
|
||||||
if k not in cont:
|
|
||||||
return False
|
|
||||||
inner_cont = cont[k]
|
|
||||||
if flag in inner_cont["recursive_flags"]:
|
|
||||||
return True
|
|
||||||
cont = inner_cont["nested"]
|
|
||||||
key_stem = key[-1]
|
|
||||||
if key_stem in cont:
|
|
||||||
cont = cont[key_stem]
|
|
||||||
return flag in cont["flags"] or flag in cont["recursive_flags"]
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class NestedDict:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
# The parsed content of the TOML document
|
|
||||||
self.dict: dict[str, Any] = {}
|
|
||||||
|
|
||||||
def get_or_create_nest(
|
|
||||||
self,
|
|
||||||
key: Key,
|
|
||||||
*,
|
|
||||||
access_lists: bool = True,
|
|
||||||
) -> dict:
|
|
||||||
cont: Any = self.dict
|
|
||||||
for k in key:
|
|
||||||
if k not in cont:
|
|
||||||
cont[k] = {}
|
|
||||||
cont = cont[k]
|
|
||||||
if access_lists and isinstance(cont, list):
|
|
||||||
cont = cont[-1]
|
|
||||||
if not isinstance(cont, dict):
|
|
||||||
raise KeyError("There is no nest behind this key")
|
|
||||||
return cont
|
|
||||||
|
|
||||||
def append_nest_to_list(self, key: Key) -> None:
|
|
||||||
cont = self.get_or_create_nest(key[:-1])
|
|
||||||
last_key = key[-1]
|
|
||||||
if last_key in cont:
|
|
||||||
list_ = cont[last_key]
|
|
||||||
if not isinstance(list_, list):
|
|
||||||
raise KeyError("An object other than list found behind this key")
|
|
||||||
list_.append({})
|
|
||||||
else:
|
|
||||||
cont[last_key] = [{}]
|
|
||||||
|
|
||||||
|
|
||||||
class Output(NamedTuple):
|
|
||||||
data: NestedDict
|
|
||||||
flags: Flags
|
|
||||||
|
|
||||||
|
|
||||||
def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos:
|
|
||||||
try:
|
|
||||||
while src[pos] in chars:
|
|
||||||
pos += 1
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
return pos
|
|
||||||
|
|
||||||
|
|
||||||
def skip_until(
|
|
||||||
src: str,
|
|
||||||
pos: Pos,
|
|
||||||
expect: str,
|
|
||||||
*,
|
|
||||||
error_on: frozenset[str],
|
|
||||||
error_on_eof: bool,
|
|
||||||
) -> Pos:
|
|
||||||
try:
|
|
||||||
new_pos = src.index(expect, pos)
|
|
||||||
except ValueError:
|
|
||||||
new_pos = len(src)
|
|
||||||
if error_on_eof:
|
|
||||||
raise suffixed_err(src, new_pos, f"Expected {expect!r}") from None
|
|
||||||
|
|
||||||
if not error_on.isdisjoint(src[pos:new_pos]):
|
|
||||||
while src[pos] not in error_on:
|
|
||||||
pos += 1
|
|
||||||
raise suffixed_err(src, pos, f"Found invalid character {src[pos]!r}")
|
|
||||||
return new_pos
|
|
||||||
|
|
||||||
|
|
||||||
def skip_comment(src: str, pos: Pos) -> Pos:
|
|
||||||
try:
|
|
||||||
char: str | None = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
char = None
|
|
||||||
if char == "#":
|
|
||||||
return skip_until(
|
|
||||||
src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False
|
|
||||||
)
|
|
||||||
return pos
|
|
||||||
|
|
||||||
|
|
||||||
def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:
|
|
||||||
while True:
|
|
||||||
pos_before_skip = pos
|
|
||||||
pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
|
||||||
pos = skip_comment(src, pos)
|
|
||||||
if pos == pos_before_skip:
|
|
||||||
return pos
|
|
||||||
|
|
||||||
|
|
||||||
def create_dict_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
|
|
||||||
pos += 1 # Skip "["
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
pos, key = parse_key(src, pos)
|
|
||||||
|
|
||||||
if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):
|
|
||||||
raise suffixed_err(src, pos, f"Cannot declare {key} twice")
|
|
||||||
out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
|
|
||||||
try:
|
|
||||||
out.data.get_or_create_nest(key)
|
|
||||||
except KeyError:
|
|
||||||
raise suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
||||||
|
|
||||||
if not src.startswith("]", pos):
|
|
||||||
raise suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
|
|
||||||
return pos + 1, key
|
|
||||||
|
|
||||||
|
|
||||||
def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
|
|
||||||
pos += 2 # Skip "[["
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
pos, key = parse_key(src, pos)
|
|
||||||
|
|
||||||
if out.flags.is_(key, Flags.FROZEN):
|
|
||||||
raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
|
|
||||||
# Free the namespace now that it points to another empty list item...
|
|
||||||
out.flags.unset_all(key)
|
|
||||||
# ...but this key precisely is still prohibited from table declaration
|
|
||||||
out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
|
|
||||||
try:
|
|
||||||
out.data.append_nest_to_list(key)
|
|
||||||
except KeyError:
|
|
||||||
raise suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
||||||
|
|
||||||
if not src.startswith("]]", pos):
|
|
||||||
raise suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
|
|
||||||
return pos + 2, key
|
|
||||||
|
|
||||||
|
|
||||||
def key_value_rule(
|
|
||||||
src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat
|
|
||||||
) -> Pos:
|
|
||||||
pos, key, value = parse_key_value_pair(src, pos, parse_float)
|
|
||||||
key_parent, key_stem = key[:-1], key[-1]
|
|
||||||
abs_key_parent = header + key_parent
|
|
||||||
|
|
||||||
relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
|
|
||||||
for cont_key in relative_path_cont_keys:
|
|
||||||
# Check that dotted key syntax does not redefine an existing table
|
|
||||||
if out.flags.is_(cont_key, Flags.EXPLICIT_NEST):
|
|
||||||
raise suffixed_err(src, pos, f"Cannot redefine namespace {cont_key}")
|
|
||||||
# Containers in the relative path can't be opened with the table syntax or
|
|
||||||
# dotted key/value syntax in following table sections.
|
|
||||||
out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST)
|
|
||||||
|
|
||||||
if out.flags.is_(abs_key_parent, Flags.FROZEN):
|
|
||||||
raise suffixed_err(
|
|
||||||
src, pos, f"Cannot mutate immutable namespace {abs_key_parent}"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
nest = out.data.get_or_create_nest(abs_key_parent)
|
|
||||||
except KeyError:
|
|
||||||
raise suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
||||||
if key_stem in nest:
|
|
||||||
raise suffixed_err(src, pos, "Cannot overwrite a value")
|
|
||||||
# Mark inline table and array namespaces recursively immutable
|
|
||||||
if isinstance(value, (dict, list)):
|
|
||||||
out.flags.set(header + key, Flags.FROZEN, recursive=True)
|
|
||||||
nest[key_stem] = value
|
|
||||||
return pos
|
|
||||||
|
|
||||||
|
|
||||||
def parse_key_value_pair(
|
|
||||||
src: str, pos: Pos, parse_float: ParseFloat
|
|
||||||
) -> tuple[Pos, Key, Any]:
|
|
||||||
pos, key = parse_key(src, pos)
|
|
||||||
try:
|
|
||||||
char: str | None = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
char = None
|
|
||||||
if char != "=":
|
|
||||||
raise suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
|
|
||||||
pos += 1
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
pos, value = parse_value(src, pos, parse_float)
|
|
||||||
return pos, key, value
|
|
||||||
|
|
||||||
|
|
||||||
def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]:
|
|
||||||
pos, key_part = parse_key_part(src, pos)
|
|
||||||
key: Key = (key_part,)
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
char: str | None = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
char = None
|
|
||||||
if char != ".":
|
|
||||||
return pos, key
|
|
||||||
pos += 1
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
pos, key_part = parse_key_part(src, pos)
|
|
||||||
key += (key_part,)
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_key_part(src: str, pos: Pos) -> tuple[Pos, str]:
|
|
||||||
try:
|
|
||||||
char: str | None = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
char = None
|
|
||||||
if char in BARE_KEY_CHARS:
|
|
||||||
start_pos = pos
|
|
||||||
pos = skip_chars(src, pos, BARE_KEY_CHARS)
|
|
||||||
return pos, src[start_pos:pos]
|
|
||||||
if char == "'":
|
|
||||||
return parse_literal_str(src, pos)
|
|
||||||
if char == '"':
|
|
||||||
return parse_one_line_basic_str(src, pos)
|
|
||||||
raise suffixed_err(src, pos, "Invalid initial character for a key part")
|
|
||||||
|
|
||||||
|
|
||||||
def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
|
|
||||||
pos += 1
|
|
||||||
return parse_basic_str(src, pos, multiline=False)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list]:
|
|
||||||
pos += 1
|
|
||||||
array: list = []
|
|
||||||
|
|
||||||
pos = skip_comments_and_array_ws(src, pos)
|
|
||||||
if src.startswith("]", pos):
|
|
||||||
return pos + 1, array
|
|
||||||
while True:
|
|
||||||
pos, val = parse_value(src, pos, parse_float)
|
|
||||||
array.append(val)
|
|
||||||
pos = skip_comments_and_array_ws(src, pos)
|
|
||||||
|
|
||||||
c = src[pos : pos + 1]
|
|
||||||
if c == "]":
|
|
||||||
return pos + 1, array
|
|
||||||
if c != ",":
|
|
||||||
raise suffixed_err(src, pos, "Unclosed array")
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
pos = skip_comments_and_array_ws(src, pos)
|
|
||||||
if src.startswith("]", pos):
|
|
||||||
return pos + 1, array
|
|
||||||
|
|
||||||
|
|
||||||
def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, dict]:
|
|
||||||
pos += 1
|
|
||||||
nested_dict = NestedDict()
|
|
||||||
flags = Flags()
|
|
||||||
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
if src.startswith("}", pos):
|
|
||||||
return pos + 1, nested_dict.dict
|
|
||||||
while True:
|
|
||||||
pos, key, value = parse_key_value_pair(src, pos, parse_float)
|
|
||||||
key_parent, key_stem = key[:-1], key[-1]
|
|
||||||
if flags.is_(key, Flags.FROZEN):
|
|
||||||
raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
|
|
||||||
try:
|
|
||||||
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
|
|
||||||
except KeyError:
|
|
||||||
raise suffixed_err(src, pos, "Cannot overwrite a value") from None
|
|
||||||
if key_stem in nest:
|
|
||||||
raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}")
|
|
||||||
nest[key_stem] = value
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
c = src[pos : pos + 1]
|
|
||||||
if c == "}":
|
|
||||||
return pos + 1, nested_dict.dict
|
|
||||||
if c != ",":
|
|
||||||
raise suffixed_err(src, pos, "Unclosed inline table")
|
|
||||||
if isinstance(value, (dict, list)):
|
|
||||||
flags.set(key, Flags.FROZEN, recursive=True)
|
|
||||||
pos += 1
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_basic_str_escape(
|
|
||||||
src: str, pos: Pos, *, multiline: bool = False
|
|
||||||
) -> tuple[Pos, str]:
|
|
||||||
escape_id = src[pos : pos + 2]
|
|
||||||
pos += 2
|
|
||||||
if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
|
|
||||||
# Skip whitespace until next non-whitespace character or end of
|
|
||||||
# the doc. Error if non-whitespace is found before newline.
|
|
||||||
if escape_id != "\\\n":
|
|
||||||
pos = skip_chars(src, pos, TOML_WS)
|
|
||||||
try:
|
|
||||||
char = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
return pos, ""
|
|
||||||
if char != "\n":
|
|
||||||
raise suffixed_err(src, pos, "Unescaped '\\' in a string")
|
|
||||||
pos += 1
|
|
||||||
pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
|
||||||
return pos, ""
|
|
||||||
if escape_id == "\\u":
|
|
||||||
return parse_hex_char(src, pos, 4)
|
|
||||||
if escape_id == "\\U":
|
|
||||||
return parse_hex_char(src, pos, 8)
|
|
||||||
try:
|
|
||||||
return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
|
|
||||||
except KeyError:
|
|
||||||
raise suffixed_err(src, pos, "Unescaped '\\' in a string") from None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]:
|
|
||||||
return parse_basic_str_escape(src, pos, multiline=True)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_hex_char(src: str, pos: Pos, hex_len: int) -> tuple[Pos, str]:
|
|
||||||
hex_str = src[pos : pos + hex_len]
|
|
||||||
if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):
|
|
||||||
raise suffixed_err(src, pos, "Invalid hex value")
|
|
||||||
pos += hex_len
|
|
||||||
hex_int = int(hex_str, 16)
|
|
||||||
if not is_unicode_scalar_value(hex_int):
|
|
||||||
raise suffixed_err(src, pos, "Escaped character is not a Unicode scalar value")
|
|
||||||
return pos, chr(hex_int)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_literal_str(src: str, pos: Pos) -> tuple[Pos, str]:
|
|
||||||
pos += 1 # Skip starting apostrophe
|
|
||||||
start_pos = pos
|
|
||||||
pos = skip_until(
|
|
||||||
src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True
|
|
||||||
)
|
|
||||||
return pos + 1, src[start_pos:pos] # Skip ending apostrophe
|
|
||||||
|
|
||||||
|
|
||||||
def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> tuple[Pos, str]:
|
|
||||||
pos += 3
|
|
||||||
if src.startswith("\n", pos):
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
if literal:
|
|
||||||
delim = "'"
|
|
||||||
end_pos = skip_until(
|
|
||||||
src,
|
|
||||||
pos,
|
|
||||||
"'''",
|
|
||||||
error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
|
|
||||||
error_on_eof=True,
|
|
||||||
)
|
|
||||||
result = src[pos:end_pos]
|
|
||||||
pos = end_pos + 3
|
|
||||||
else:
|
|
||||||
delim = '"'
|
|
||||||
pos, result = parse_basic_str(src, pos, multiline=True)
|
|
||||||
|
|
||||||
# Add at maximum two extra apostrophes/quotes if the end sequence
|
|
||||||
# is 4 or 5 chars long instead of just 3.
|
|
||||||
if not src.startswith(delim, pos):
|
|
||||||
return pos, result
|
|
||||||
pos += 1
|
|
||||||
if not src.startswith(delim, pos):
|
|
||||||
return pos, result + delim
|
|
||||||
pos += 1
|
|
||||||
return pos, result + (delim * 2)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
|
|
||||||
if multiline:
|
|
||||||
error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
|
||||||
parse_escapes = parse_basic_str_escape_multiline
|
|
||||||
else:
|
|
||||||
error_on = ILLEGAL_BASIC_STR_CHARS
|
|
||||||
parse_escapes = parse_basic_str_escape
|
|
||||||
result = ""
|
|
||||||
start_pos = pos
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
char = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
raise suffixed_err(src, pos, "Unterminated string") from None
|
|
||||||
if char == '"':
|
|
||||||
if not multiline:
|
|
||||||
return pos + 1, result + src[start_pos:pos]
|
|
||||||
if src.startswith('"""', pos):
|
|
||||||
return pos + 3, result + src[start_pos:pos]
|
|
||||||
pos += 1
|
|
||||||
continue
|
|
||||||
if char == "\\":
|
|
||||||
result += src[start_pos:pos]
|
|
||||||
pos, parsed_escape = parse_escapes(src, pos)
|
|
||||||
result += parsed_escape
|
|
||||||
start_pos = pos
|
|
||||||
continue
|
|
||||||
if char in error_on:
|
|
||||||
raise suffixed_err(src, pos, f"Illegal character {char!r}")
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
|
|
||||||
def parse_value( # noqa: C901
|
|
||||||
src: str, pos: Pos, parse_float: ParseFloat
|
|
||||||
) -> tuple[Pos, Any]:
|
|
||||||
try:
|
|
||||||
char: str | None = src[pos]
|
|
||||||
except IndexError:
|
|
||||||
char = None
|
|
||||||
|
|
||||||
# IMPORTANT: order conditions based on speed of checking and likelihood
|
|
||||||
|
|
||||||
# Basic strings
|
|
||||||
if char == '"':
|
|
||||||
if src.startswith('"""', pos):
|
|
||||||
return parse_multiline_str(src, pos, literal=False)
|
|
||||||
return parse_one_line_basic_str(src, pos)
|
|
||||||
|
|
||||||
# Literal strings
|
|
||||||
if char == "'":
|
|
||||||
if src.startswith("'''", pos):
|
|
||||||
return parse_multiline_str(src, pos, literal=True)
|
|
||||||
return parse_literal_str(src, pos)
|
|
||||||
|
|
||||||
# Booleans
|
|
||||||
if char == "t":
|
|
||||||
if src.startswith("true", pos):
|
|
||||||
return pos + 4, True
|
|
||||||
if char == "f":
|
|
||||||
if src.startswith("false", pos):
|
|
||||||
return pos + 5, False
|
|
||||||
|
|
||||||
# Arrays
|
|
||||||
if char == "[":
|
|
||||||
return parse_array(src, pos, parse_float)
|
|
||||||
|
|
||||||
# Inline tables
|
|
||||||
if char == "{":
|
|
||||||
return parse_inline_table(src, pos, parse_float)
|
|
||||||
|
|
||||||
# Dates and times
|
|
||||||
datetime_match = RE_DATETIME.match(src, pos)
|
|
||||||
if datetime_match:
|
|
||||||
try:
|
|
||||||
datetime_obj = match_to_datetime(datetime_match)
|
|
||||||
except ValueError as e:
|
|
||||||
raise suffixed_err(src, pos, "Invalid date or datetime") from e
|
|
||||||
return datetime_match.end(), datetime_obj
|
|
||||||
localtime_match = RE_LOCALTIME.match(src, pos)
|
|
||||||
if localtime_match:
|
|
||||||
return localtime_match.end(), match_to_localtime(localtime_match)
|
|
||||||
|
|
||||||
# Integers and "normal" floats.
|
|
||||||
# The regex will greedily match any type starting with a decimal
|
|
||||||
# char, so needs to be located after handling of dates and times.
|
|
||||||
number_match = RE_NUMBER.match(src, pos)
|
|
||||||
if number_match:
|
|
||||||
return number_match.end(), match_to_number(number_match, parse_float)
|
|
||||||
|
|
||||||
# Special floats
|
|
||||||
first_three = src[pos : pos + 3]
|
|
||||||
if first_three in {"inf", "nan"}:
|
|
||||||
return pos + 3, parse_float(first_three)
|
|
||||||
first_four = src[pos : pos + 4]
|
|
||||||
if first_four in {"-inf", "+inf", "-nan", "+nan"}:
|
|
||||||
return pos + 4, parse_float(first_four)
|
|
||||||
|
|
||||||
raise suffixed_err(src, pos, "Invalid value")
|
|
||||||
|
|
||||||
|
|
||||||
def suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError:
|
|
||||||
"""Return a `TOMLDecodeError` where error message is suffixed with
|
|
||||||
coordinates in source."""
|
|
||||||
|
|
||||||
def coord_repr(src: str, pos: Pos) -> str:
|
|
||||||
if pos >= len(src):
|
|
||||||
return "end of document"
|
|
||||||
line = src.count("\n", 0, pos) + 1
|
|
||||||
if line == 1:
|
|
||||||
column = pos + 1
|
|
||||||
else:
|
|
||||||
column = pos - src.rindex("\n", 0, pos)
|
|
||||||
return f"line {line}, column {column}"
|
|
||||||
|
|
||||||
return TOMLDecodeError(f"{msg} (at {coord_repr(src, pos)})")
|
|
||||||
|
|
||||||
|
|
||||||
def is_unicode_scalar_value(codepoint: int) -> bool:
|
|
||||||
return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
|
|
||||||
|
|
||||||
|
|
||||||
def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:
|
|
||||||
"""A decorator to make `parse_float` safe.
|
|
||||||
|
|
||||||
`parse_float` must not return dicts or lists, because these types
|
|
||||||
would be mixed with parsed TOML tables and arrays, thus confusing
|
|
||||||
the parser. The returned decorated callable raises `ValueError`
|
|
||||||
instead of returning illegal types.
|
|
||||||
"""
|
|
||||||
# The default `float` callable never returns illegal types. Optimize it.
|
|
||||||
if parse_float is float: # type: ignore[comparison-overlap]
|
|
||||||
return float
|
|
||||||
|
|
||||||
def safe_parse_float(float_str: str) -> Any:
|
|
||||||
float_value = parse_float(float_str)
|
|
||||||
if isinstance(float_value, (dict, list)):
|
|
||||||
raise ValueError("parse_float must not return dicts or lists")
|
|
||||||
return float_value
|
|
||||||
|
|
||||||
return safe_parse_float
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
|
||||||
# Licensed to PSF under a Contributor Agreement.
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import date, datetime, time, timedelta, timezone, tzinfo
|
|
||||||
from functools import lru_cache
|
|
||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ._types import ParseFloat
|
|
||||||
|
|
||||||
# E.g.
|
|
||||||
# - 00:32:00.999999
|
|
||||||
# - 00:32:00
|
|
||||||
_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?"
|
|
||||||
|
|
||||||
RE_NUMBER = re.compile(
|
|
||||||
r"""
|
|
||||||
0
|
|
||||||
(?:
|
|
||||||
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
|
||||||
|
|
|
||||||
b[01](?:_?[01])* # bin
|
|
||||||
|
|
|
||||||
o[0-7](?:_?[0-7])* # oct
|
|
||||||
)
|
|
||||||
|
|
|
||||||
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
|
||||||
(?P<floatpart>
|
|
||||||
(?:\.[0-9](?:_?[0-9])*)? # optional fractional part
|
|
||||||
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
flags=re.VERBOSE,
|
|
||||||
)
|
|
||||||
RE_LOCALTIME = re.compile(_TIME_RE_STR)
|
|
||||||
RE_DATETIME = re.compile(
|
|
||||||
rf"""
|
|
||||||
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
|
||||||
(?:
|
|
||||||
[Tt ]
|
|
||||||
{_TIME_RE_STR}
|
|
||||||
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
|
||||||
)?
|
|
||||||
""",
|
|
||||||
flags=re.VERBOSE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def match_to_datetime(match: re.Match) -> datetime | date:
|
|
||||||
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
|
|
||||||
|
|
||||||
Raises ValueError if the match does not correspond to a valid date
|
|
||||||
or datetime.
|
|
||||||
"""
|
|
||||||
(
|
|
||||||
year_str,
|
|
||||||
month_str,
|
|
||||||
day_str,
|
|
||||||
hour_str,
|
|
||||||
minute_str,
|
|
||||||
sec_str,
|
|
||||||
micros_str,
|
|
||||||
zulu_time,
|
|
||||||
offset_sign_str,
|
|
||||||
offset_hour_str,
|
|
||||||
offset_minute_str,
|
|
||||||
) = match.groups()
|
|
||||||
year, month, day = int(year_str), int(month_str), int(day_str)
|
|
||||||
if hour_str is None:
|
|
||||||
return date(year, month, day)
|
|
||||||
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
|
||||||
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
|
|
||||||
if offset_sign_str:
|
|
||||||
tz: tzinfo | None = cached_tz(
|
|
||||||
offset_hour_str, offset_minute_str, offset_sign_str
|
|
||||||
)
|
|
||||||
elif zulu_time:
|
|
||||||
tz = timezone.utc
|
|
||||||
else: # local date-time
|
|
||||||
tz = None
|
|
||||||
return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
|
||||||
def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
|
|
||||||
sign = 1 if sign_str == "+" else -1
|
|
||||||
return timezone(
|
|
||||||
timedelta(
|
|
||||||
hours=sign * int(hour_str),
|
|
||||||
minutes=sign * int(minute_str),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def match_to_localtime(match: re.Match) -> time:
|
|
||||||
hour_str, minute_str, sec_str, micros_str = match.groups()
|
|
||||||
micros = int(micros_str.ljust(6, "0")) if micros_str else 0
|
|
||||||
return time(int(hour_str), int(minute_str), int(sec_str), micros)
|
|
||||||
|
|
||||||
|
|
||||||
def match_to_number(match: re.Match, parse_float: ParseFloat) -> Any:
|
|
||||||
if match.group("floatpart"):
|
|
||||||
return parse_float(match.group())
|
|
||||||
return int(match.group(), 0)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
|
||||||
# Licensed to PSF under a Contributor Agreement.
|
|
||||||
|
|
||||||
from typing import Any, Callable, Tuple
|
|
||||||
|
|
||||||
# Type annotations
|
|
||||||
ParseFloat = Callable[[str], Any]
|
|
||||||
Key = Tuple[str, ...]
|
|
||||||
Pos = int
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,329 +0,0 @@
|
|||||||
import io
|
|
||||||
import posixpath
|
|
||||||
import zipfile
|
|
||||||
import itertools
|
|
||||||
import contextlib
|
|
||||||
import sys
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
if sys.version_info < (3, 7):
|
|
||||||
from collections import OrderedDict
|
|
||||||
else:
|
|
||||||
OrderedDict = dict
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Path']
|
|
||||||
|
|
||||||
|
|
||||||
def _parents(path):
|
|
||||||
"""
|
|
||||||
Given a path with elements separated by
|
|
||||||
posixpath.sep, generate all parents of that path.
|
|
||||||
|
|
||||||
>>> list(_parents('b/d'))
|
|
||||||
['b']
|
|
||||||
>>> list(_parents('/b/d/'))
|
|
||||||
['/b']
|
|
||||||
>>> list(_parents('b/d/f/'))
|
|
||||||
['b/d', 'b']
|
|
||||||
>>> list(_parents('b'))
|
|
||||||
[]
|
|
||||||
>>> list(_parents(''))
|
|
||||||
[]
|
|
||||||
"""
|
|
||||||
return itertools.islice(_ancestry(path), 1, None)
|
|
||||||
|
|
||||||
|
|
||||||
def _ancestry(path):
|
|
||||||
"""
|
|
||||||
Given a path with elements separated by
|
|
||||||
posixpath.sep, generate all elements of that path
|
|
||||||
|
|
||||||
>>> list(_ancestry('b/d'))
|
|
||||||
['b/d', 'b']
|
|
||||||
>>> list(_ancestry('/b/d/'))
|
|
||||||
['/b/d', '/b']
|
|
||||||
>>> list(_ancestry('b/d/f/'))
|
|
||||||
['b/d/f', 'b/d', 'b']
|
|
||||||
>>> list(_ancestry('b'))
|
|
||||||
['b']
|
|
||||||
>>> list(_ancestry(''))
|
|
||||||
[]
|
|
||||||
"""
|
|
||||||
path = path.rstrip(posixpath.sep)
|
|
||||||
while path and path != posixpath.sep:
|
|
||||||
yield path
|
|
||||||
path, tail = posixpath.split(path)
|
|
||||||
|
|
||||||
|
|
||||||
_dedupe = OrderedDict.fromkeys
|
|
||||||
"""Deduplicate an iterable in original order"""
|
|
||||||
|
|
||||||
|
|
||||||
def _difference(minuend, subtrahend):
|
|
||||||
"""
|
|
||||||
Return items in minuend not in subtrahend, retaining order
|
|
||||||
with O(1) lookup.
|
|
||||||
"""
|
|
||||||
return itertools.filterfalse(set(subtrahend).__contains__, minuend)
|
|
||||||
|
|
||||||
|
|
||||||
class CompleteDirs(zipfile.ZipFile):
|
|
||||||
"""
|
|
||||||
A ZipFile subclass that ensures that implied directories
|
|
||||||
are always included in the namelist.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _implied_dirs(names):
|
|
||||||
parents = itertools.chain.from_iterable(map(_parents, names))
|
|
||||||
as_dirs = (p + posixpath.sep for p in parents)
|
|
||||||
return _dedupe(_difference(as_dirs, names))
|
|
||||||
|
|
||||||
def namelist(self):
|
|
||||||
names = super(CompleteDirs, self).namelist()
|
|
||||||
return names + list(self._implied_dirs(names))
|
|
||||||
|
|
||||||
def _name_set(self):
|
|
||||||
return set(self.namelist())
|
|
||||||
|
|
||||||
def resolve_dir(self, name):
|
|
||||||
"""
|
|
||||||
If the name represents a directory, return that name
|
|
||||||
as a directory (with the trailing slash).
|
|
||||||
"""
|
|
||||||
names = self._name_set()
|
|
||||||
dirname = name + '/'
|
|
||||||
dir_match = name not in names and dirname in names
|
|
||||||
return dirname if dir_match else name
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def make(cls, source):
|
|
||||||
"""
|
|
||||||
Given a source (filename or zipfile), return an
|
|
||||||
appropriate CompleteDirs subclass.
|
|
||||||
"""
|
|
||||||
if isinstance(source, CompleteDirs):
|
|
||||||
return source
|
|
||||||
|
|
||||||
if not isinstance(source, zipfile.ZipFile):
|
|
||||||
return cls(_pathlib_compat(source))
|
|
||||||
|
|
||||||
# Only allow for FastLookup when supplied zipfile is read-only
|
|
||||||
if 'r' not in source.mode:
|
|
||||||
cls = CompleteDirs
|
|
||||||
|
|
||||||
source.__class__ = cls
|
|
||||||
return source
|
|
||||||
|
|
||||||
|
|
||||||
class FastLookup(CompleteDirs):
|
|
||||||
"""
|
|
||||||
ZipFile subclass to ensure implicit
|
|
||||||
dirs exist and are resolved rapidly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def namelist(self):
|
|
||||||
with contextlib.suppress(AttributeError):
|
|
||||||
return self.__names
|
|
||||||
self.__names = super(FastLookup, self).namelist()
|
|
||||||
return self.__names
|
|
||||||
|
|
||||||
def _name_set(self):
|
|
||||||
with contextlib.suppress(AttributeError):
|
|
||||||
return self.__lookup
|
|
||||||
self.__lookup = super(FastLookup, self)._name_set()
|
|
||||||
return self.__lookup
|
|
||||||
|
|
||||||
|
|
||||||
def _pathlib_compat(path):
|
|
||||||
"""
|
|
||||||
For path-like objects, convert to a filename for compatibility
|
|
||||||
on Python 3.6.1 and earlier.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return path.__fspath__()
|
|
||||||
except AttributeError:
|
|
||||||
return str(path)
|
|
||||||
|
|
||||||
|
|
||||||
class Path:
|
|
||||||
"""
|
|
||||||
A pathlib-compatible interface for zip files.
|
|
||||||
|
|
||||||
Consider a zip file with this structure::
|
|
||||||
|
|
||||||
.
|
|
||||||
├── a.txt
|
|
||||||
└── b
|
|
||||||
├── c.txt
|
|
||||||
└── d
|
|
||||||
└── e.txt
|
|
||||||
|
|
||||||
>>> data = io.BytesIO()
|
|
||||||
>>> zf = zipfile.ZipFile(data, 'w')
|
|
||||||
>>> zf.writestr('a.txt', 'content of a')
|
|
||||||
>>> zf.writestr('b/c.txt', 'content of c')
|
|
||||||
>>> zf.writestr('b/d/e.txt', 'content of e')
|
|
||||||
>>> zf.filename = 'mem/abcde.zip'
|
|
||||||
|
|
||||||
Path accepts the zipfile object itself or a filename
|
|
||||||
|
|
||||||
>>> root = Path(zf)
|
|
||||||
|
|
||||||
From there, several path operations are available.
|
|
||||||
|
|
||||||
Directory iteration (including the zip file itself):
|
|
||||||
|
|
||||||
>>> a, b = root.iterdir()
|
|
||||||
>>> a
|
|
||||||
Path('mem/abcde.zip', 'a.txt')
|
|
||||||
>>> b
|
|
||||||
Path('mem/abcde.zip', 'b/')
|
|
||||||
|
|
||||||
name property:
|
|
||||||
|
|
||||||
>>> b.name
|
|
||||||
'b'
|
|
||||||
|
|
||||||
join with divide operator:
|
|
||||||
|
|
||||||
>>> c = b / 'c.txt'
|
|
||||||
>>> c
|
|
||||||
Path('mem/abcde.zip', 'b/c.txt')
|
|
||||||
>>> c.name
|
|
||||||
'c.txt'
|
|
||||||
|
|
||||||
Read text:
|
|
||||||
|
|
||||||
>>> c.read_text()
|
|
||||||
'content of c'
|
|
||||||
|
|
||||||
existence:
|
|
||||||
|
|
||||||
>>> c.exists()
|
|
||||||
True
|
|
||||||
>>> (b / 'missing.txt').exists()
|
|
||||||
False
|
|
||||||
|
|
||||||
Coercion to string:
|
|
||||||
|
|
||||||
>>> import os
|
|
||||||
>>> str(c).replace(os.sep, posixpath.sep)
|
|
||||||
'mem/abcde.zip/b/c.txt'
|
|
||||||
|
|
||||||
At the root, ``name``, ``filename``, and ``parent``
|
|
||||||
resolve to the zipfile. Note these attributes are not
|
|
||||||
valid and will raise a ``ValueError`` if the zipfile
|
|
||||||
has no filename.
|
|
||||||
|
|
||||||
>>> root.name
|
|
||||||
'abcde.zip'
|
|
||||||
>>> str(root.filename).replace(os.sep, posixpath.sep)
|
|
||||||
'mem/abcde.zip'
|
|
||||||
>>> str(root.parent)
|
|
||||||
'mem'
|
|
||||||
"""
|
|
||||||
|
|
||||||
__repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})"
|
|
||||||
|
|
||||||
def __init__(self, root, at=""):
|
|
||||||
"""
|
|
||||||
Construct a Path from a ZipFile or filename.
|
|
||||||
|
|
||||||
Note: When the source is an existing ZipFile object,
|
|
||||||
its type (__class__) will be mutated to a
|
|
||||||
specialized type. If the caller wishes to retain the
|
|
||||||
original type, the caller should either create a
|
|
||||||
separate ZipFile object or pass a filename.
|
|
||||||
"""
|
|
||||||
self.root = FastLookup.make(root)
|
|
||||||
self.at = at
|
|
||||||
|
|
||||||
def open(self, mode='r', *args, pwd=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Open this entry as text or binary following the semantics
|
|
||||||
of ``pathlib.Path.open()`` by passing arguments through
|
|
||||||
to io.TextIOWrapper().
|
|
||||||
"""
|
|
||||||
if self.is_dir():
|
|
||||||
raise IsADirectoryError(self)
|
|
||||||
zip_mode = mode[0]
|
|
||||||
if not self.exists() and zip_mode == 'r':
|
|
||||||
raise FileNotFoundError(self)
|
|
||||||
stream = self.root.open(self.at, zip_mode, pwd=pwd)
|
|
||||||
if 'b' in mode:
|
|
||||||
if args or kwargs:
|
|
||||||
raise ValueError("encoding args invalid for binary operation")
|
|
||||||
return stream
|
|
||||||
return io.TextIOWrapper(stream, *args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return pathlib.Path(self.at).name or self.filename.name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def suffix(self):
|
|
||||||
return pathlib.Path(self.at).suffix or self.filename.suffix
|
|
||||||
|
|
||||||
@property
|
|
||||||
def suffixes(self):
|
|
||||||
return pathlib.Path(self.at).suffixes or self.filename.suffixes
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stem(self):
|
|
||||||
return pathlib.Path(self.at).stem or self.filename.stem
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filename(self):
|
|
||||||
return pathlib.Path(self.root.filename).joinpath(self.at)
|
|
||||||
|
|
||||||
def read_text(self, *args, **kwargs):
|
|
||||||
with self.open('r', *args, **kwargs) as strm:
|
|
||||||
return strm.read()
|
|
||||||
|
|
||||||
def read_bytes(self):
|
|
||||||
with self.open('rb') as strm:
|
|
||||||
return strm.read()
|
|
||||||
|
|
||||||
def _is_child(self, path):
|
|
||||||
return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/")
|
|
||||||
|
|
||||||
def _next(self, at):
|
|
||||||
return self.__class__(self.root, at)
|
|
||||||
|
|
||||||
def is_dir(self):
|
|
||||||
return not self.at or self.at.endswith("/")
|
|
||||||
|
|
||||||
def is_file(self):
|
|
||||||
return self.exists() and not self.is_dir()
|
|
||||||
|
|
||||||
def exists(self):
|
|
||||||
return self.at in self.root._name_set()
|
|
||||||
|
|
||||||
def iterdir(self):
|
|
||||||
if not self.is_dir():
|
|
||||||
raise ValueError("Can't listdir a file")
|
|
||||||
subs = map(self._next, self.root.namelist())
|
|
||||||
return filter(self._is_child, subs)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return posixpath.join(self.root.filename, self.at)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.__repr.format(self=self)
|
|
||||||
|
|
||||||
def joinpath(self, *other):
|
|
||||||
next = posixpath.join(self.at, *map(_pathlib_compat, other))
|
|
||||||
return self._next(self.root.resolve_dir(next))
|
|
||||||
|
|
||||||
__truediv__ = joinpath
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent(self):
|
|
||||||
if not self.at:
|
|
||||||
return self.filename.parent
|
|
||||||
parent_at = posixpath.dirname(self.at.rstrip('/'))
|
|
||||||
if parent_at:
|
|
||||||
parent_at += '/'
|
|
||||||
return self._next(parent_at)
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
"""Utilities for extracting common archive formats"""
|
|
||||||
|
|
||||||
import zipfile
|
|
||||||
import tarfile
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import posixpath
|
|
||||||
import contextlib
|
|
||||||
from distutils.errors import DistutilsError
|
|
||||||
|
|
||||||
from ._path import ensure_directory
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"unpack_archive", "unpack_zipfile", "unpack_tarfile", "default_filter",
|
|
||||||
"UnrecognizedFormat", "extraction_drivers", "unpack_directory",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UnrecognizedFormat(DistutilsError):
|
|
||||||
"""Couldn't recognize the archive type"""
|
|
||||||
|
|
||||||
|
|
||||||
def default_filter(src, dst):
|
|
||||||
"""The default progress/filter callback; returns True for all files"""
|
|
||||||
return dst
|
|
||||||
|
|
||||||
|
|
||||||
def unpack_archive(
|
|
||||||
filename, extract_dir, progress_filter=default_filter,
|
|
||||||
drivers=None):
|
|
||||||
"""Unpack `filename` to `extract_dir`, or raise ``UnrecognizedFormat``
|
|
||||||
|
|
||||||
`progress_filter` is a function taking two arguments: a source path
|
|
||||||
internal to the archive ('/'-separated), and a filesystem path where it
|
|
||||||
will be extracted. The callback must return the desired extract path
|
|
||||||
(which may be the same as the one passed in), or else ``None`` to skip
|
|
||||||
that file or directory. The callback can thus be used to report on the
|
|
||||||
progress of the extraction, as well as to filter the items extracted or
|
|
||||||
alter their extraction paths.
|
|
||||||
|
|
||||||
`drivers`, if supplied, must be a non-empty sequence of functions with the
|
|
||||||
same signature as this function (minus the `drivers` argument), that raise
|
|
||||||
``UnrecognizedFormat`` if they do not support extracting the designated
|
|
||||||
archive type. The `drivers` are tried in sequence until one is found that
|
|
||||||
does not raise an error, or until all are exhausted (in which case
|
|
||||||
``UnrecognizedFormat`` is raised). If you do not supply a sequence of
|
|
||||||
drivers, the module's ``extraction_drivers`` constant will be used, which
|
|
||||||
means that ``unpack_zipfile`` and ``unpack_tarfile`` will be tried, in that
|
|
||||||
order.
|
|
||||||
"""
|
|
||||||
for driver in drivers or extraction_drivers:
|
|
||||||
try:
|
|
||||||
driver(filename, extract_dir, progress_filter)
|
|
||||||
except UnrecognizedFormat:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
raise UnrecognizedFormat(
|
|
||||||
"Not a recognized archive type: %s" % filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def unpack_directory(filename, extract_dir, progress_filter=default_filter):
|
|
||||||
""""Unpack" a directory, using the same interface as for archives
|
|
||||||
|
|
||||||
Raises ``UnrecognizedFormat`` if `filename` is not a directory
|
|
||||||
"""
|
|
||||||
if not os.path.isdir(filename):
|
|
||||||
raise UnrecognizedFormat("%s is not a directory" % filename)
|
|
||||||
|
|
||||||
paths = {
|
|
||||||
filename: ('', extract_dir),
|
|
||||||
}
|
|
||||||
for base, dirs, files in os.walk(filename):
|
|
||||||
src, dst = paths[base]
|
|
||||||
for d in dirs:
|
|
||||||
paths[os.path.join(base, d)] = src + d + '/', os.path.join(dst, d)
|
|
||||||
for f in files:
|
|
||||||
target = os.path.join(dst, f)
|
|
||||||
target = progress_filter(src + f, target)
|
|
||||||
if not target:
|
|
||||||
# skip non-files
|
|
||||||
continue
|
|
||||||
ensure_directory(target)
|
|
||||||
f = os.path.join(base, f)
|
|
||||||
shutil.copyfile(f, target)
|
|
||||||
shutil.copystat(f, target)
|
|
||||||
|
|
||||||
|
|
||||||
def unpack_zipfile(filename, extract_dir, progress_filter=default_filter):
|
|
||||||
"""Unpack zip `filename` to `extract_dir`
|
|
||||||
|
|
||||||
Raises ``UnrecognizedFormat`` if `filename` is not a zipfile (as determined
|
|
||||||
by ``zipfile.is_zipfile()``). See ``unpack_archive()`` for an explanation
|
|
||||||
of the `progress_filter` argument.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not zipfile.is_zipfile(filename):
|
|
||||||
raise UnrecognizedFormat("%s is not a zip file" % (filename,))
|
|
||||||
|
|
||||||
with zipfile.ZipFile(filename) as z:
|
|
||||||
_unpack_zipfile_obj(z, extract_dir, progress_filter)
|
|
||||||
|
|
||||||
|
|
||||||
def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter):
|
|
||||||
"""Internal/private API used by other parts of setuptools.
|
|
||||||
Similar to ``unpack_zipfile``, but receives an already opened :obj:`zipfile.ZipFile`
|
|
||||||
object instead of a filename.
|
|
||||||
"""
|
|
||||||
for info in zipfile_obj.infolist():
|
|
||||||
name = info.filename
|
|
||||||
|
|
||||||
# don't extract absolute paths or ones with .. in them
|
|
||||||
if name.startswith('/') or '..' in name.split('/'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
target = os.path.join(extract_dir, *name.split('/'))
|
|
||||||
target = progress_filter(name, target)
|
|
||||||
if not target:
|
|
||||||
continue
|
|
||||||
if name.endswith('/'):
|
|
||||||
# directory
|
|
||||||
ensure_directory(target)
|
|
||||||
else:
|
|
||||||
# file
|
|
||||||
ensure_directory(target)
|
|
||||||
data = zipfile_obj.read(info.filename)
|
|
||||||
with open(target, 'wb') as f:
|
|
||||||
f.write(data)
|
|
||||||
unix_attributes = info.external_attr >> 16
|
|
||||||
if unix_attributes:
|
|
||||||
os.chmod(target, unix_attributes)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_tar_file_or_dir(tar_obj, tar_member_obj):
|
|
||||||
"""Resolve any links and extract link targets as normal files."""
|
|
||||||
while tar_member_obj is not None and (
|
|
||||||
tar_member_obj.islnk() or tar_member_obj.issym()):
|
|
||||||
linkpath = tar_member_obj.linkname
|
|
||||||
if tar_member_obj.issym():
|
|
||||||
base = posixpath.dirname(tar_member_obj.name)
|
|
||||||
linkpath = posixpath.join(base, linkpath)
|
|
||||||
linkpath = posixpath.normpath(linkpath)
|
|
||||||
tar_member_obj = tar_obj._getmember(linkpath)
|
|
||||||
|
|
||||||
is_file_or_dir = (
|
|
||||||
tar_member_obj is not None and
|
|
||||||
(tar_member_obj.isfile() or tar_member_obj.isdir())
|
|
||||||
)
|
|
||||||
if is_file_or_dir:
|
|
||||||
return tar_member_obj
|
|
||||||
|
|
||||||
raise LookupError('Got unknown file type')
|
|
||||||
|
|
||||||
|
|
||||||
def _iter_open_tar(tar_obj, extract_dir, progress_filter):
|
|
||||||
"""Emit member-destination pairs from a tar archive."""
|
|
||||||
# don't do any chowning!
|
|
||||||
tar_obj.chown = lambda *args: None
|
|
||||||
|
|
||||||
with contextlib.closing(tar_obj):
|
|
||||||
for member in tar_obj:
|
|
||||||
name = member.name
|
|
||||||
# don't extract absolute paths or ones with .. in them
|
|
||||||
if name.startswith('/') or '..' in name.split('/'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
prelim_dst = os.path.join(extract_dir, *name.split('/'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
member = _resolve_tar_file_or_dir(tar_obj, member)
|
|
||||||
except LookupError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
final_dst = progress_filter(name, prelim_dst)
|
|
||||||
if not final_dst:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if final_dst.endswith(os.sep):
|
|
||||||
final_dst = final_dst[:-1]
|
|
||||||
|
|
||||||
yield member, final_dst
|
|
||||||
|
|
||||||
|
|
||||||
def unpack_tarfile(filename, extract_dir, progress_filter=default_filter):
|
|
||||||
"""Unpack tar/tar.gz/tar.bz2 `filename` to `extract_dir`
|
|
||||||
|
|
||||||
Raises ``UnrecognizedFormat`` if `filename` is not a tarfile (as determined
|
|
||||||
by ``tarfile.open()``). See ``unpack_archive()`` for an explanation
|
|
||||||
of the `progress_filter` argument.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
tarobj = tarfile.open(filename)
|
|
||||||
except tarfile.TarError as e:
|
|
||||||
raise UnrecognizedFormat(
|
|
||||||
"%s is not a compressed or uncompressed tar file" % (filename,)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
for member, final_dst in _iter_open_tar(
|
|
||||||
tarobj, extract_dir, progress_filter,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
# XXX Ugh
|
|
||||||
tarobj._extract_member(member, final_dst)
|
|
||||||
except tarfile.ExtractError:
|
|
||||||
# chown/chmod/mkfifo/mknode/makedev failed
|
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
extraction_drivers = unpack_directory, unpack_zipfile, unpack_tarfile
|
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
"""A PEP 517 interface to setuptools
|
|
||||||
|
|
||||||
Previously, when a user or a command line tool (let's call it a "frontend")
|
|
||||||
needed to make a request of setuptools to take a certain action, for
|
|
||||||
example, generating a list of installation requirements, the frontend would
|
|
||||||
would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.
|
|
||||||
|
|
||||||
PEP 517 defines a different method of interfacing with setuptools. Rather
|
|
||||||
than calling "setup.py" directly, the frontend should:
|
|
||||||
|
|
||||||
1. Set the current directory to the directory with a setup.py file
|
|
||||||
2. Import this module into a safe python interpreter (one in which
|
|
||||||
setuptools can potentially set global variables or crash hard).
|
|
||||||
3. Call one of the functions defined in PEP 517.
|
|
||||||
|
|
||||||
What each function does is defined in PEP 517. However, here is a "casual"
|
|
||||||
definition of the functions (this definition should not be relied on for
|
|
||||||
bug reports or API stability):
|
|
||||||
|
|
||||||
- `build_wheel`: build a wheel in the folder and return the basename
|
|
||||||
- `get_requires_for_build_wheel`: get the `setup_requires` to build
|
|
||||||
- `prepare_metadata_for_build_wheel`: get the `install_requires`
|
|
||||||
- `build_sdist`: build an sdist in the folder and return the basename
|
|
||||||
- `get_requires_for_build_sdist`: get the `setup_requires` to build
|
|
||||||
|
|
||||||
Again, this is not a formal definition! Just a "taste" of the module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import shlex
|
|
||||||
import sys
|
|
||||||
import tokenize
|
|
||||||
import shutil
|
|
||||||
import contextlib
|
|
||||||
import tempfile
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Iterator, List, Optional, Union
|
|
||||||
|
|
||||||
import setuptools
|
|
||||||
import distutils
|
|
||||||
from . import errors
|
|
||||||
from ._path import same_path
|
|
||||||
from ._reqs import parse_strings
|
|
||||||
from ._deprecation_warning import SetuptoolsDeprecationWarning
|
|
||||||
from distutils.util import strtobool
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['get_requires_for_build_sdist',
|
|
||||||
'get_requires_for_build_wheel',
|
|
||||||
'prepare_metadata_for_build_wheel',
|
|
||||||
'build_wheel',
|
|
||||||
'build_sdist',
|
|
||||||
'get_requires_for_build_editable',
|
|
||||||
'prepare_metadata_for_build_editable',
|
|
||||||
'build_editable',
|
|
||||||
'__legacy__',
|
|
||||||
'SetupRequirementsError']
|
|
||||||
|
|
||||||
SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()
|
|
||||||
LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-")
|
|
||||||
|
|
||||||
|
|
||||||
class SetupRequirementsError(BaseException):
|
|
||||||
def __init__(self, specifiers):
|
|
||||||
self.specifiers = specifiers
|
|
||||||
|
|
||||||
|
|
||||||
class Distribution(setuptools.dist.Distribution):
|
|
||||||
def fetch_build_eggs(self, specifiers):
|
|
||||||
specifier_list = list(parse_strings(specifiers))
|
|
||||||
|
|
||||||
raise SetupRequirementsError(specifier_list)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def patch(cls):
|
|
||||||
"""
|
|
||||||
Replace
|
|
||||||
distutils.dist.Distribution with this class
|
|
||||||
for the duration of this context.
|
|
||||||
"""
|
|
||||||
orig = distutils.core.Distribution
|
|
||||||
distutils.core.Distribution = cls
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
distutils.core.Distribution = orig
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def no_install_setup_requires():
|
|
||||||
"""Temporarily disable installing setup_requires
|
|
||||||
|
|
||||||
Under PEP 517, the backend reports build dependencies to the frontend,
|
|
||||||
and the frontend is responsible for ensuring they're installed.
|
|
||||||
So setuptools (acting as a backend) should not try to install them.
|
|
||||||
"""
|
|
||||||
orig = setuptools._install_setup_requires
|
|
||||||
setuptools._install_setup_requires = lambda attrs: None
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
setuptools._install_setup_requires = orig
|
|
||||||
|
|
||||||
|
|
||||||
def _get_immediate_subdirectories(a_dir):
|
|
||||||
return [name for name in os.listdir(a_dir)
|
|
||||||
if os.path.isdir(os.path.join(a_dir, name))]
|
|
||||||
|
|
||||||
|
|
||||||
def _file_with_extension(directory, extension):
|
|
||||||
matching = (
|
|
||||||
f for f in os.listdir(directory)
|
|
||||||
if f.endswith(extension)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
file, = matching
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(
|
|
||||||
'No distribution was found. Ensure that `setup.py` '
|
|
||||||
'is not empty and that it calls `setup()`.')
|
|
||||||
return file
|
|
||||||
|
|
||||||
|
|
||||||
def _open_setup_script(setup_script):
|
|
||||||
if not os.path.exists(setup_script):
|
|
||||||
# Supply a default setup.py
|
|
||||||
return io.StringIO(u"from setuptools import setup; setup()")
|
|
||||||
|
|
||||||
return getattr(tokenize, 'open', open)(setup_script)
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def suppress_known_deprecation():
|
|
||||||
with warnings.catch_warnings():
|
|
||||||
warnings.filterwarnings('ignore', 'setup.py install is deprecated')
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
_ConfigSettings = Optional[Dict[str, Union[str, List[str], None]]]
|
|
||||||
"""
|
|
||||||
Currently the user can run::
|
|
||||||
|
|
||||||
pip install -e . --config-settings key=value
|
|
||||||
python -m build -C--key=value -C key=value
|
|
||||||
|
|
||||||
- pip will pass both key and value as strings and overwriting repeated keys
|
|
||||||
(pypa/pip#11059).
|
|
||||||
- build will accumulate values associated with repeated keys in a list.
|
|
||||||
It will also accept keys with no associated value.
|
|
||||||
This means that an option passed by build can be ``str | list[str] | None``.
|
|
||||||
- PEP 517 specifies that ``config_settings`` is an optional dict.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class _ConfigSettingsTranslator:
|
|
||||||
"""Translate ``config_settings`` into distutils-style command arguments.
|
|
||||||
Only a limited number of options is currently supported.
|
|
||||||
"""
|
|
||||||
# See pypa/setuptools#1928 pypa/setuptools#2491
|
|
||||||
|
|
||||||
def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]:
|
|
||||||
"""
|
|
||||||
Get the value of a specific key in ``config_settings`` as a list of strings.
|
|
||||||
|
|
||||||
>>> fn = _ConfigSettingsTranslator()._get_config
|
|
||||||
>>> fn("--global-option", None)
|
|
||||||
[]
|
|
||||||
>>> fn("--global-option", {})
|
|
||||||
[]
|
|
||||||
>>> fn("--global-option", {'--global-option': 'foo'})
|
|
||||||
['foo']
|
|
||||||
>>> fn("--global-option", {'--global-option': ['foo']})
|
|
||||||
['foo']
|
|
||||||
>>> fn("--global-option", {'--global-option': 'foo'})
|
|
||||||
['foo']
|
|
||||||
>>> fn("--global-option", {'--global-option': 'foo bar'})
|
|
||||||
['foo', 'bar']
|
|
||||||
"""
|
|
||||||
cfg = config_settings or {}
|
|
||||||
opts = cfg.get(key) or []
|
|
||||||
return shlex.split(opts) if isinstance(opts, str) else opts
|
|
||||||
|
|
||||||
def _valid_global_options(self):
|
|
||||||
"""Global options accepted by setuptools (e.g. quiet or verbose)."""
|
|
||||||
options = (opt[:2] for opt in setuptools.dist.Distribution.global_options)
|
|
||||||
return {flag for long_and_short in options for flag in long_and_short if flag}
|
|
||||||
|
|
||||||
def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
Let the user specify ``verbose`` or ``quiet`` + escape hatch via
|
|
||||||
``--global-option``.
|
|
||||||
Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,
|
|
||||||
so we just have to cover the basic scenario ``-v``.
|
|
||||||
|
|
||||||
>>> fn = _ConfigSettingsTranslator()._global_args
|
|
||||||
>>> list(fn(None))
|
|
||||||
[]
|
|
||||||
>>> list(fn({"verbose": "False"}))
|
|
||||||
['-q']
|
|
||||||
>>> list(fn({"verbose": "1"}))
|
|
||||||
['-v']
|
|
||||||
>>> list(fn({"--verbose": None}))
|
|
||||||
['-v']
|
|
||||||
>>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))
|
|
||||||
['-v', '-q', '--no-user-cfg']
|
|
||||||
>>> list(fn({"--quiet": None}))
|
|
||||||
['-q']
|
|
||||||
"""
|
|
||||||
cfg = config_settings or {}
|
|
||||||
falsey = {"false", "no", "0", "off"}
|
|
||||||
if "verbose" in cfg or "--verbose" in cfg:
|
|
||||||
level = str(cfg.get("verbose") or cfg.get("--verbose") or "1")
|
|
||||||
yield ("-q" if level.lower() in falsey else "-v")
|
|
||||||
if "quiet" in cfg or "--quiet" in cfg:
|
|
||||||
level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")
|
|
||||||
yield ("-v" if level.lower() in falsey else "-q")
|
|
||||||
|
|
||||||
valid = self._valid_global_options()
|
|
||||||
args = self._get_config("--global-option", config_settings)
|
|
||||||
yield from (arg for arg in args if arg.strip("-") in valid)
|
|
||||||
|
|
||||||
def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel``
|
|
||||||
commands run in ``build_sdist`` and ``build_wheel`` to re-use the egg-info
|
|
||||||
directory created in ``prepare_metadata_for_build_wheel``.
|
|
||||||
|
|
||||||
>>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args
|
|
||||||
>>> list(fn(None))
|
|
||||||
[]
|
|
||||||
>>> list(fn({"tag-date": "False"}))
|
|
||||||
['--no-date']
|
|
||||||
>>> list(fn({"tag-date": None}))
|
|
||||||
['--no-date']
|
|
||||||
>>> list(fn({"tag-date": "true", "tag-build": ".a"}))
|
|
||||||
['--tag-date', '--tag-build', '.a']
|
|
||||||
"""
|
|
||||||
cfg = config_settings or {}
|
|
||||||
if "tag-date" in cfg:
|
|
||||||
val = strtobool(str(cfg["tag-date"] or "false"))
|
|
||||||
yield ("--tag-date" if val else "--no-date")
|
|
||||||
if "tag-build" in cfg:
|
|
||||||
yield from ["--tag-build", str(cfg["tag-build"])]
|
|
||||||
|
|
||||||
def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
The ``editable_wheel`` command accepts ``editable-mode=strict``.
|
|
||||||
|
|
||||||
>>> fn = _ConfigSettingsTranslator()._editable_args
|
|
||||||
>>> list(fn(None))
|
|
||||||
[]
|
|
||||||
>>> list(fn({"editable-mode": "strict"}))
|
|
||||||
['--mode', 'strict']
|
|
||||||
"""
|
|
||||||
cfg = config_settings or {}
|
|
||||||
mode = cfg.get("editable-mode") or cfg.get("editable_mode")
|
|
||||||
if not mode:
|
|
||||||
return
|
|
||||||
yield from ["--mode", str(mode)]
|
|
||||||
|
|
||||||
def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
Users may expect to pass arbitrary lists of arguments to a command
|
|
||||||
via "--global-option" (example provided in PEP 517 of a "escape hatch").
|
|
||||||
|
|
||||||
>>> fn = _ConfigSettingsTranslator()._arbitrary_args
|
|
||||||
>>> list(fn(None))
|
|
||||||
[]
|
|
||||||
>>> list(fn({}))
|
|
||||||
[]
|
|
||||||
>>> list(fn({'--build-option': 'foo'}))
|
|
||||||
['foo']
|
|
||||||
>>> list(fn({'--build-option': ['foo']}))
|
|
||||||
['foo']
|
|
||||||
>>> list(fn({'--build-option': 'foo'}))
|
|
||||||
['foo']
|
|
||||||
>>> list(fn({'--build-option': 'foo bar'}))
|
|
||||||
['foo', 'bar']
|
|
||||||
>>> warnings.simplefilter('error', SetuptoolsDeprecationWarning)
|
|
||||||
>>> list(fn({'--global-option': 'foo'})) # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
||||||
Traceback (most recent call last):
|
|
||||||
SetuptoolsDeprecationWarning: ...arguments given via `--global-option`...
|
|
||||||
"""
|
|
||||||
args = self._get_config("--global-option", config_settings)
|
|
||||||
global_opts = self._valid_global_options()
|
|
||||||
bad_args = []
|
|
||||||
|
|
||||||
for arg in args:
|
|
||||||
if arg.strip("-") not in global_opts:
|
|
||||||
bad_args.append(arg)
|
|
||||||
yield arg
|
|
||||||
|
|
||||||
yield from self._get_config("--build-option", config_settings)
|
|
||||||
|
|
||||||
if bad_args:
|
|
||||||
msg = f"""
|
|
||||||
The arguments {bad_args!r} were given via `--global-option`.
|
|
||||||
Please use `--build-option` instead,
|
|
||||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
|
||||||
"""
|
|
||||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
|
||||||
|
|
||||||
|
|
||||||
class _BuildMetaBackend(_ConfigSettingsTranslator):
|
|
||||||
def _get_build_requires(self, config_settings, requirements):
|
|
||||||
sys.argv = [
|
|
||||||
*sys.argv[:1],
|
|
||||||
*self._global_args(config_settings),
|
|
||||||
"egg_info",
|
|
||||||
*self._arbitrary_args(config_settings),
|
|
||||||
]
|
|
||||||
try:
|
|
||||||
with Distribution.patch():
|
|
||||||
self.run_setup()
|
|
||||||
except SetupRequirementsError as e:
|
|
||||||
requirements += e.specifiers
|
|
||||||
|
|
||||||
return requirements
|
|
||||||
|
|
||||||
def run_setup(self, setup_script='setup.py'):
|
|
||||||
# Note that we can reuse our build directory between calls
|
|
||||||
# Correctness comes first, then optimization later
|
|
||||||
__file__ = setup_script
|
|
||||||
__name__ = '__main__'
|
|
||||||
|
|
||||||
with _open_setup_script(__file__) as f:
|
|
||||||
code = f.read().replace(r'\r\n', r'\n')
|
|
||||||
|
|
||||||
exec(code, locals())
|
|
||||||
|
|
||||||
def get_requires_for_build_wheel(self, config_settings=None):
|
|
||||||
return self._get_build_requires(config_settings, requirements=['wheel'])
|
|
||||||
|
|
||||||
def get_requires_for_build_sdist(self, config_settings=None):
|
|
||||||
return self._get_build_requires(config_settings, requirements=[])
|
|
||||||
|
|
||||||
def _bubble_up_info_directory(self, metadata_directory: str, suffix: str) -> str:
|
|
||||||
"""
|
|
||||||
PEP 517 requires that the .dist-info directory be placed in the
|
|
||||||
metadata_directory. To comply, we MUST copy the directory to the root.
|
|
||||||
|
|
||||||
Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.
|
|
||||||
"""
|
|
||||||
info_dir = self._find_info_directory(metadata_directory, suffix)
|
|
||||||
if not same_path(info_dir.parent, metadata_directory):
|
|
||||||
shutil.move(str(info_dir), metadata_directory)
|
|
||||||
# PEP 517 allow other files and dirs to exist in metadata_directory
|
|
||||||
return info_dir.name
|
|
||||||
|
|
||||||
def _find_info_directory(self, metadata_directory: str, suffix: str) -> Path:
|
|
||||||
for parent, dirs, _ in os.walk(metadata_directory):
|
|
||||||
candidates = [f for f in dirs if f.endswith(suffix)]
|
|
||||||
|
|
||||||
if len(candidates) != 0 or len(dirs) != 1:
|
|
||||||
assert len(candidates) == 1, f"Multiple {suffix} directories found"
|
|
||||||
return Path(parent, candidates[0])
|
|
||||||
|
|
||||||
msg = f"No {suffix} directory found in {metadata_directory}"
|
|
||||||
raise errors.InternalError(msg)
|
|
||||||
|
|
||||||
def prepare_metadata_for_build_wheel(self, metadata_directory,
|
|
||||||
config_settings=None):
|
|
||||||
sys.argv = [
|
|
||||||
*sys.argv[:1],
|
|
||||||
*self._global_args(config_settings),
|
|
||||||
"dist_info",
|
|
||||||
"--output-dir", metadata_directory,
|
|
||||||
"--keep-egg-info",
|
|
||||||
]
|
|
||||||
with no_install_setup_requires():
|
|
||||||
self.run_setup()
|
|
||||||
|
|
||||||
self._bubble_up_info_directory(metadata_directory, ".egg-info")
|
|
||||||
return self._bubble_up_info_directory(metadata_directory, ".dist-info")
|
|
||||||
|
|
||||||
def _build_with_temp_dir(self, setup_command, result_extension,
|
|
||||||
result_directory, config_settings):
|
|
||||||
result_directory = os.path.abspath(result_directory)
|
|
||||||
|
|
||||||
# Build in a temporary directory, then copy to the target.
|
|
||||||
os.makedirs(result_directory, exist_ok=True)
|
|
||||||
temp_opts = {"prefix": ".tmp-", "dir": result_directory}
|
|
||||||
with tempfile.TemporaryDirectory(**temp_opts) as tmp_dist_dir:
|
|
||||||
sys.argv = [
|
|
||||||
*sys.argv[:1],
|
|
||||||
*self._global_args(config_settings),
|
|
||||||
*setup_command,
|
|
||||||
"--dist-dir", tmp_dist_dir,
|
|
||||||
*self._arbitrary_args(config_settings),
|
|
||||||
]
|
|
||||||
with no_install_setup_requires():
|
|
||||||
self.run_setup()
|
|
||||||
|
|
||||||
result_basename = _file_with_extension(
|
|
||||||
tmp_dist_dir, result_extension)
|
|
||||||
result_path = os.path.join(result_directory, result_basename)
|
|
||||||
if os.path.exists(result_path):
|
|
||||||
# os.rename will fail overwriting on non-Unix.
|
|
||||||
os.remove(result_path)
|
|
||||||
os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)
|
|
||||||
|
|
||||||
return result_basename
|
|
||||||
|
|
||||||
def build_wheel(self, wheel_directory, config_settings=None,
|
|
||||||
metadata_directory=None):
|
|
||||||
with suppress_known_deprecation():
|
|
||||||
return self._build_with_temp_dir(['bdist_wheel'], '.whl',
|
|
||||||
wheel_directory, config_settings)
|
|
||||||
|
|
||||||
def build_sdist(self, sdist_directory, config_settings=None):
|
|
||||||
return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],
|
|
||||||
'.tar.gz', sdist_directory,
|
|
||||||
config_settings)
|
|
||||||
|
|
||||||
def _get_dist_info_dir(self, metadata_directory: Optional[str]) -> Optional[str]:
|
|
||||||
if not metadata_directory:
|
|
||||||
return None
|
|
||||||
dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info"))
|
|
||||||
assert len(dist_info_candidates) <= 1
|
|
||||||
return str(dist_info_candidates[0]) if dist_info_candidates else None
|
|
||||||
|
|
||||||
if not LEGACY_EDITABLE:
|
|
||||||
|
|
||||||
# PEP660 hooks:
|
|
||||||
# build_editable
|
|
||||||
# get_requires_for_build_editable
|
|
||||||
# prepare_metadata_for_build_editable
|
|
||||||
def build_editable(
|
|
||||||
self, wheel_directory, config_settings=None, metadata_directory=None
|
|
||||||
):
|
|
||||||
# XXX can or should we hide our editable_wheel command normally?
|
|
||||||
info_dir = self._get_dist_info_dir(metadata_directory)
|
|
||||||
opts = ["--dist-info-dir", info_dir] if info_dir else []
|
|
||||||
cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]
|
|
||||||
with suppress_known_deprecation():
|
|
||||||
return self._build_with_temp_dir(
|
|
||||||
cmd, ".whl", wheel_directory, config_settings
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_requires_for_build_editable(self, config_settings=None):
|
|
||||||
return self.get_requires_for_build_wheel(config_settings)
|
|
||||||
|
|
||||||
def prepare_metadata_for_build_editable(self, metadata_directory,
|
|
||||||
config_settings=None):
|
|
||||||
return self.prepare_metadata_for_build_wheel(
|
|
||||||
metadata_directory, config_settings
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _BuildMetaLegacyBackend(_BuildMetaBackend):
|
|
||||||
"""Compatibility backend for setuptools
|
|
||||||
|
|
||||||
This is a version of setuptools.build_meta that endeavors
|
|
||||||
to maintain backwards
|
|
||||||
compatibility with pre-PEP 517 modes of invocation. It
|
|
||||||
exists as a temporary
|
|
||||||
bridge between the old packaging mechanism and the new
|
|
||||||
packaging mechanism,
|
|
||||||
and will eventually be removed.
|
|
||||||
"""
|
|
||||||
def run_setup(self, setup_script='setup.py'):
|
|
||||||
# In order to maintain compatibility with scripts assuming that
|
|
||||||
# the setup.py script is in a directory on the PYTHONPATH, inject
|
|
||||||
# '' into sys.path. (pypa/setuptools#1642)
|
|
||||||
sys_path = list(sys.path) # Save the original path
|
|
||||||
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(setup_script))
|
|
||||||
if script_dir not in sys.path:
|
|
||||||
sys.path.insert(0, script_dir)
|
|
||||||
|
|
||||||
# Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to
|
|
||||||
# get the directory of the source code. They expect it to refer to the
|
|
||||||
# setup.py script.
|
|
||||||
sys_argv_0 = sys.argv[0]
|
|
||||||
sys.argv[0] = setup_script
|
|
||||||
|
|
||||||
try:
|
|
||||||
super(_BuildMetaLegacyBackend,
|
|
||||||
self).run_setup(setup_script=setup_script)
|
|
||||||
finally:
|
|
||||||
# While PEP 517 frontends should be calling each hook in a fresh
|
|
||||||
# subprocess according to the standard (and thus it should not be
|
|
||||||
# strictly necessary to restore the old sys.path), we'll restore
|
|
||||||
# the original path so that the path manipulation does not persist
|
|
||||||
# within the hook after run_setup is called.
|
|
||||||
sys.path[:] = sys_path
|
|
||||||
sys.argv[0] = sys_argv_0
|
|
||||||
|
|
||||||
|
|
||||||
# The primary backend
|
|
||||||
_BACKEND = _BuildMetaBackend()
|
|
||||||
|
|
||||||
get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel
|
|
||||||
get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist
|
|
||||||
prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel
|
|
||||||
build_wheel = _BACKEND.build_wheel
|
|
||||||
build_sdist = _BACKEND.build_sdist
|
|
||||||
|
|
||||||
if not LEGACY_EDITABLE:
|
|
||||||
get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable
|
|
||||||
prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable
|
|
||||||
build_editable = _BACKEND.build_editable
|
|
||||||
|
|
||||||
|
|
||||||
# The legacy backend
|
|
||||||
__legacy__ = _BuildMetaLegacyBackend()
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,12 +0,0 @@
|
|||||||
from distutils.command.bdist import bdist
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if 'egg' not in bdist.format_commands:
|
|
||||||
try:
|
|
||||||
bdist.format_commands['egg'] = ('bdist_egg', "Python .egg file")
|
|
||||||
except TypeError:
|
|
||||||
# For backward compatibility with older distutils (stdlib)
|
|
||||||
bdist.format_command['egg'] = ('bdist_egg', "Python .egg file")
|
|
||||||
bdist.format_commands.append('egg')
|
|
||||||
|
|
||||||
del bdist, sys
|
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -1,78 +0,0 @@
|
|||||||
from distutils.errors import DistutilsOptionError
|
|
||||||
|
|
||||||
from setuptools.command.setopt import edit_config, option_base, config_file
|
|
||||||
|
|
||||||
|
|
||||||
def shquote(arg):
|
|
||||||
"""Quote an argument for later parsing by shlex.split()"""
|
|
||||||
for c in '"', "'", "\\", "#":
|
|
||||||
if c in arg:
|
|
||||||
return repr(arg)
|
|
||||||
if arg.split() != [arg]:
|
|
||||||
return repr(arg)
|
|
||||||
return arg
|
|
||||||
|
|
||||||
|
|
||||||
class alias(option_base):
|
|
||||||
"""Define a shortcut that invokes one or more commands"""
|
|
||||||
|
|
||||||
description = "define a shortcut to invoke one or more commands"
|
|
||||||
command_consumes_arguments = True
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
('remove', 'r', 'remove (unset) the alias'),
|
|
||||||
] + option_base.user_options
|
|
||||||
|
|
||||||
boolean_options = option_base.boolean_options + ['remove']
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
option_base.initialize_options(self)
|
|
||||||
self.args = None
|
|
||||||
self.remove = None
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
option_base.finalize_options(self)
|
|
||||||
if self.remove and len(self.args) != 1:
|
|
||||||
raise DistutilsOptionError(
|
|
||||||
"Must specify exactly one argument (the alias name) when "
|
|
||||||
"using --remove"
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
aliases = self.distribution.get_option_dict('aliases')
|
|
||||||
|
|
||||||
if not self.args:
|
|
||||||
print("Command Aliases")
|
|
||||||
print("---------------")
|
|
||||||
for alias in aliases:
|
|
||||||
print("setup.py alias", format_alias(alias, aliases))
|
|
||||||
return
|
|
||||||
|
|
||||||
elif len(self.args) == 1:
|
|
||||||
alias, = self.args
|
|
||||||
if self.remove:
|
|
||||||
command = None
|
|
||||||
elif alias in aliases:
|
|
||||||
print("setup.py alias", format_alias(alias, aliases))
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
print("No alias definition found for %r" % alias)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
alias = self.args[0]
|
|
||||||
command = ' '.join(map(shquote, self.args[1:]))
|
|
||||||
|
|
||||||
edit_config(self.filename, {'aliases': {alias: command}}, self.dry_run)
|
|
||||||
|
|
||||||
|
|
||||||
def format_alias(name, aliases):
|
|
||||||
source, command = aliases[name]
|
|
||||||
if source == config_file('global'):
|
|
||||||
source = '--global-config '
|
|
||||||
elif source == config_file('user'):
|
|
||||||
source = '--user-config '
|
|
||||||
elif source == config_file('local'):
|
|
||||||
source = ''
|
|
||||||
else:
|
|
||||||
source = '--filename=%r' % source
|
|
||||||
return source + name + ' ' + command
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
"""setuptools.command.bdist_egg
|
|
||||||
|
|
||||||
Build .egg distributions"""
|
|
||||||
|
|
||||||
from distutils.dir_util import remove_tree, mkpath
|
|
||||||
from distutils import log
|
|
||||||
from types import CodeType
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import textwrap
|
|
||||||
import marshal
|
|
||||||
|
|
||||||
from pkg_resources import get_build_platform, Distribution
|
|
||||||
from setuptools.extension import Library
|
|
||||||
from setuptools import Command
|
|
||||||
from .._path import ensure_directory
|
|
||||||
|
|
||||||
from sysconfig import get_path, get_python_version
|
|
||||||
|
|
||||||
|
|
||||||
def _get_purelib():
|
|
||||||
return get_path("purelib")
|
|
||||||
|
|
||||||
|
|
||||||
def strip_module(filename):
|
|
||||||
if '.' in filename:
|
|
||||||
filename = os.path.splitext(filename)[0]
|
|
||||||
if filename.endswith('module'):
|
|
||||||
filename = filename[:-6]
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
def sorted_walk(dir):
|
|
||||||
"""Do os.walk in a reproducible way,
|
|
||||||
independent of indeterministic filesystem readdir order
|
|
||||||
"""
|
|
||||||
for base, dirs, files in os.walk(dir):
|
|
||||||
dirs.sort()
|
|
||||||
files.sort()
|
|
||||||
yield base, dirs, files
|
|
||||||
|
|
||||||
|
|
||||||
def write_stub(resource, pyfile):
|
|
||||||
_stub_template = textwrap.dedent("""
|
|
||||||
def __bootstrap__():
|
|
||||||
global __bootstrap__, __loader__, __file__
|
|
||||||
import sys, pkg_resources, importlib.util
|
|
||||||
__file__ = pkg_resources.resource_filename(__name__, %r)
|
|
||||||
__loader__ = None; del __bootstrap__, __loader__
|
|
||||||
spec = importlib.util.spec_from_file_location(__name__,__file__)
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(mod)
|
|
||||||
__bootstrap__()
|
|
||||||
""").lstrip()
|
|
||||||
with open(pyfile, 'w') as f:
|
|
||||||
f.write(_stub_template % resource)
|
|
||||||
|
|
||||||
|
|
||||||
class bdist_egg(Command):
|
|
||||||
description = "create an \"egg\" distribution"
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
('bdist-dir=', 'b',
|
|
||||||
"temporary directory for creating the distribution"),
|
|
||||||
('plat-name=', 'p', "platform name to embed in generated filenames "
|
|
||||||
"(default: %s)" % get_build_platform()),
|
|
||||||
('exclude-source-files', None,
|
|
||||||
"remove all .py files from the generated egg"),
|
|
||||||
('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)"),
|
|
||||||
]
|
|
||||||
|
|
||||||
boolean_options = [
|
|
||||||
'keep-temp', 'skip-build', 'exclude-source-files'
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.bdist_dir = None
|
|
||||||
self.plat_name = None
|
|
||||||
self.keep_temp = 0
|
|
||||||
self.dist_dir = None
|
|
||||||
self.skip_build = 0
|
|
||||||
self.egg_output = None
|
|
||||||
self.exclude_source_files = None
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
ei_cmd = self.ei_cmd = self.get_finalized_command("egg_info")
|
|
||||||
self.egg_info = ei_cmd.egg_info
|
|
||||||
|
|
||||||
if self.bdist_dir is None:
|
|
||||||
bdist_base = self.get_finalized_command('bdist').bdist_base
|
|
||||||
self.bdist_dir = os.path.join(bdist_base, 'egg')
|
|
||||||
|
|
||||||
if self.plat_name is None:
|
|
||||||
self.plat_name = get_build_platform()
|
|
||||||
|
|
||||||
self.set_undefined_options('bdist', ('dist_dir', 'dist_dir'))
|
|
||||||
|
|
||||||
if self.egg_output is None:
|
|
||||||
|
|
||||||
# Compute filename of the output egg
|
|
||||||
basename = Distribution(
|
|
||||||
None, None, ei_cmd.egg_name, ei_cmd.egg_version,
|
|
||||||
get_python_version(),
|
|
||||||
self.distribution.has_ext_modules() and self.plat_name
|
|
||||||
).egg_name()
|
|
||||||
|
|
||||||
self.egg_output = os.path.join(self.dist_dir, basename + '.egg')
|
|
||||||
|
|
||||||
def do_install_data(self):
|
|
||||||
# Hack for packages that install data to install's --install-lib
|
|
||||||
self.get_finalized_command('install').install_lib = self.bdist_dir
|
|
||||||
|
|
||||||
site_packages = os.path.normcase(os.path.realpath(_get_purelib()))
|
|
||||||
old, self.distribution.data_files = self.distribution.data_files, []
|
|
||||||
|
|
||||||
for item in old:
|
|
||||||
if isinstance(item, tuple) and len(item) == 2:
|
|
||||||
if os.path.isabs(item[0]):
|
|
||||||
realpath = os.path.realpath(item[0])
|
|
||||||
normalized = os.path.normcase(realpath)
|
|
||||||
if normalized == site_packages or normalized.startswith(
|
|
||||||
site_packages + os.sep
|
|
||||||
):
|
|
||||||
item = realpath[len(site_packages) + 1:], item[1]
|
|
||||||
# XXX else: raise ???
|
|
||||||
self.distribution.data_files.append(item)
|
|
||||||
|
|
||||||
try:
|
|
||||||
log.info("installing package data to %s", self.bdist_dir)
|
|
||||||
self.call_command('install_data', force=0, root=None)
|
|
||||||
finally:
|
|
||||||
self.distribution.data_files = old
|
|
||||||
|
|
||||||
def get_outputs(self):
|
|
||||||
return [self.egg_output]
|
|
||||||
|
|
||||||
def call_command(self, cmdname, **kw):
|
|
||||||
"""Invoke reinitialized command `cmdname` with keyword args"""
|
|
||||||
for dirname in INSTALL_DIRECTORY_ATTRS:
|
|
||||||
kw.setdefault(dirname, self.bdist_dir)
|
|
||||||
kw.setdefault('skip_build', self.skip_build)
|
|
||||||
kw.setdefault('dry_run', self.dry_run)
|
|
||||||
cmd = self.reinitialize_command(cmdname, **kw)
|
|
||||||
self.run_command(cmdname)
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def run(self): # noqa: C901 # is too complex (14) # FIXME
|
|
||||||
# Generate metadata first
|
|
||||||
self.run_command("egg_info")
|
|
||||||
# We run install_lib before install_data, because some data hacks
|
|
||||||
# pull their data path from the install_lib command.
|
|
||||||
log.info("installing library code to %s", self.bdist_dir)
|
|
||||||
instcmd = self.get_finalized_command('install')
|
|
||||||
old_root = instcmd.root
|
|
||||||
instcmd.root = None
|
|
||||||
if self.distribution.has_c_libraries() and not self.skip_build:
|
|
||||||
self.run_command('build_clib')
|
|
||||||
cmd = self.call_command('install_lib', warn_dir=0)
|
|
||||||
instcmd.root = old_root
|
|
||||||
|
|
||||||
all_outputs, ext_outputs = self.get_ext_outputs()
|
|
||||||
self.stubs = []
|
|
||||||
to_compile = []
|
|
||||||
for (p, ext_name) in enumerate(ext_outputs):
|
|
||||||
filename, ext = os.path.splitext(ext_name)
|
|
||||||
pyfile = os.path.join(self.bdist_dir, strip_module(filename) +
|
|
||||||
'.py')
|
|
||||||
self.stubs.append(pyfile)
|
|
||||||
log.info("creating stub loader for %s", ext_name)
|
|
||||||
if not self.dry_run:
|
|
||||||
write_stub(os.path.basename(ext_name), pyfile)
|
|
||||||
to_compile.append(pyfile)
|
|
||||||
ext_outputs[p] = ext_name.replace(os.sep, '/')
|
|
||||||
|
|
||||||
if to_compile:
|
|
||||||
cmd.byte_compile(to_compile)
|
|
||||||
if self.distribution.data_files:
|
|
||||||
self.do_install_data()
|
|
||||||
|
|
||||||
# Make the EGG-INFO directory
|
|
||||||
archive_root = self.bdist_dir
|
|
||||||
egg_info = os.path.join(archive_root, 'EGG-INFO')
|
|
||||||
self.mkpath(egg_info)
|
|
||||||
if self.distribution.scripts:
|
|
||||||
script_dir = os.path.join(egg_info, 'scripts')
|
|
||||||
log.info("installing scripts to %s", script_dir)
|
|
||||||
self.call_command('install_scripts', install_dir=script_dir,
|
|
||||||
no_ep=1)
|
|
||||||
|
|
||||||
self.copy_metadata_to(egg_info)
|
|
||||||
native_libs = os.path.join(egg_info, "native_libs.txt")
|
|
||||||
if all_outputs:
|
|
||||||
log.info("writing %s", native_libs)
|
|
||||||
if not self.dry_run:
|
|
||||||
ensure_directory(native_libs)
|
|
||||||
libs_file = open(native_libs, 'wt')
|
|
||||||
libs_file.write('\n'.join(all_outputs))
|
|
||||||
libs_file.write('\n')
|
|
||||||
libs_file.close()
|
|
||||||
elif os.path.isfile(native_libs):
|
|
||||||
log.info("removing %s", native_libs)
|
|
||||||
if not self.dry_run:
|
|
||||||
os.unlink(native_libs)
|
|
||||||
|
|
||||||
write_safety_flag(
|
|
||||||
os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
|
|
||||||
)
|
|
||||||
|
|
||||||
if os.path.exists(os.path.join(self.egg_info, 'depends.txt')):
|
|
||||||
log.warn(
|
|
||||||
"WARNING: 'depends.txt' will not be used by setuptools 0.6!\n"
|
|
||||||
"Use the install_requires/extras_require setup() args instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.exclude_source_files:
|
|
||||||
self.zap_pyfiles()
|
|
||||||
|
|
||||||
# Make the archive
|
|
||||||
make_zipfile(self.egg_output, archive_root, verbose=self.verbose,
|
|
||||||
dry_run=self.dry_run, mode=self.gen_header())
|
|
||||||
if not self.keep_temp:
|
|
||||||
remove_tree(self.bdist_dir, dry_run=self.dry_run)
|
|
||||||
|
|
||||||
# Add to 'Distribution.dist_files' so that the "upload" command works
|
|
||||||
getattr(self.distribution, 'dist_files', []).append(
|
|
||||||
('bdist_egg', get_python_version(), self.egg_output))
|
|
||||||
|
|
||||||
def zap_pyfiles(self):
|
|
||||||
log.info("Removing .py files from temporary directory")
|
|
||||||
for base, dirs, files in walk_egg(self.bdist_dir):
|
|
||||||
for name in files:
|
|
||||||
path = os.path.join(base, name)
|
|
||||||
|
|
||||||
if name.endswith('.py'):
|
|
||||||
log.debug("Deleting %s", path)
|
|
||||||
os.unlink(path)
|
|
||||||
|
|
||||||
if base.endswith('__pycache__'):
|
|
||||||
path_old = path
|
|
||||||
|
|
||||||
pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'
|
|
||||||
m = re.match(pattern, name)
|
|
||||||
path_new = os.path.join(
|
|
||||||
base, os.pardir, m.group('name') + '.pyc')
|
|
||||||
log.info(
|
|
||||||
"Renaming file from [%s] to [%s]"
|
|
||||||
% (path_old, path_new))
|
|
||||||
try:
|
|
||||||
os.remove(path_new)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
os.rename(path_old, path_new)
|
|
||||||
|
|
||||||
def zip_safe(self):
|
|
||||||
safe = getattr(self.distribution, 'zip_safe', None)
|
|
||||||
if safe is not None:
|
|
||||||
return safe
|
|
||||||
log.warn("zip_safe flag not set; analyzing archive contents...")
|
|
||||||
return analyze_egg(self.bdist_dir, self.stubs)
|
|
||||||
|
|
||||||
def gen_header(self):
|
|
||||||
return 'w'
|
|
||||||
|
|
||||||
def copy_metadata_to(self, target_dir):
|
|
||||||
"Copy metadata (egg info) to the target_dir"
|
|
||||||
# normalize the path (so that a forward-slash in egg_info will
|
|
||||||
# match using startswith below)
|
|
||||||
norm_egg_info = os.path.normpath(self.egg_info)
|
|
||||||
prefix = os.path.join(norm_egg_info, '')
|
|
||||||
for path in self.ei_cmd.filelist.files:
|
|
||||||
if path.startswith(prefix):
|
|
||||||
target = os.path.join(target_dir, path[len(prefix):])
|
|
||||||
ensure_directory(target)
|
|
||||||
self.copy_file(path, target)
|
|
||||||
|
|
||||||
def get_ext_outputs(self):
|
|
||||||
"""Get a list of relative paths to C extensions in the output distro"""
|
|
||||||
|
|
||||||
all_outputs = []
|
|
||||||
ext_outputs = []
|
|
||||||
|
|
||||||
paths = {self.bdist_dir: ''}
|
|
||||||
for base, dirs, files in sorted_walk(self.bdist_dir):
|
|
||||||
for filename in files:
|
|
||||||
if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS:
|
|
||||||
all_outputs.append(paths[base] + filename)
|
|
||||||
for filename in dirs:
|
|
||||||
paths[os.path.join(base, filename)] = (paths[base] +
|
|
||||||
filename + '/')
|
|
||||||
|
|
||||||
if self.distribution.has_ext_modules():
|
|
||||||
build_cmd = self.get_finalized_command('build_ext')
|
|
||||||
for ext in build_cmd.extensions:
|
|
||||||
if isinstance(ext, Library):
|
|
||||||
continue
|
|
||||||
fullname = build_cmd.get_ext_fullname(ext.name)
|
|
||||||
filename = build_cmd.get_ext_filename(fullname)
|
|
||||||
if not os.path.basename(filename).startswith('dl-'):
|
|
||||||
if os.path.exists(os.path.join(self.bdist_dir, filename)):
|
|
||||||
ext_outputs.append(filename)
|
|
||||||
|
|
||||||
return all_outputs, ext_outputs
|
|
||||||
|
|
||||||
|
|
||||||
NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split())
|
|
||||||
|
|
||||||
|
|
||||||
def walk_egg(egg_dir):
|
|
||||||
"""Walk an unpacked egg's contents, skipping the metadata directory"""
|
|
||||||
walker = sorted_walk(egg_dir)
|
|
||||||
base, dirs, files = next(walker)
|
|
||||||
if 'EGG-INFO' in dirs:
|
|
||||||
dirs.remove('EGG-INFO')
|
|
||||||
yield base, dirs, files
|
|
||||||
for bdf in walker:
|
|
||||||
yield bdf
|
|
||||||
|
|
||||||
|
|
||||||
def analyze_egg(egg_dir, stubs):
|
|
||||||
# check for existing flag in EGG-INFO
|
|
||||||
for flag, fn in safety_flags.items():
|
|
||||||
if os.path.exists(os.path.join(egg_dir, 'EGG-INFO', fn)):
|
|
||||||
return flag
|
|
||||||
if not can_scan():
|
|
||||||
return False
|
|
||||||
safe = True
|
|
||||||
for base, dirs, files in walk_egg(egg_dir):
|
|
||||||
for name in files:
|
|
||||||
if name.endswith('.py') or name.endswith('.pyw'):
|
|
||||||
continue
|
|
||||||
elif name.endswith('.pyc') or name.endswith('.pyo'):
|
|
||||||
# always scan, even if we already know we're not safe
|
|
||||||
safe = scan_module(egg_dir, base, name, stubs) and safe
|
|
||||||
return safe
|
|
||||||
|
|
||||||
|
|
||||||
def write_safety_flag(egg_dir, safe):
|
|
||||||
# Write or remove zip safety flag file(s)
|
|
||||||
for flag, fn in safety_flags.items():
|
|
||||||
fn = os.path.join(egg_dir, fn)
|
|
||||||
if os.path.exists(fn):
|
|
||||||
if safe is None or bool(safe) != flag:
|
|
||||||
os.unlink(fn)
|
|
||||||
elif safe is not None and bool(safe) == flag:
|
|
||||||
f = open(fn, 'wt')
|
|
||||||
f.write('\n')
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
|
|
||||||
safety_flags = {
|
|
||||||
True: 'zip-safe',
|
|
||||||
False: 'not-zip-safe',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def scan_module(egg_dir, base, name, stubs):
|
|
||||||
"""Check whether module possibly uses unsafe-for-zipfile stuff"""
|
|
||||||
|
|
||||||
filename = os.path.join(base, name)
|
|
||||||
if filename[:-1] in stubs:
|
|
||||||
return True # Extension module
|
|
||||||
pkg = base[len(egg_dir) + 1:].replace(os.sep, '.')
|
|
||||||
module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0]
|
|
||||||
if sys.version_info < (3, 7):
|
|
||||||
skip = 12 # skip magic & date & file size
|
|
||||||
else:
|
|
||||||
skip = 16 # skip magic & reserved? & date & file size
|
|
||||||
f = open(filename, 'rb')
|
|
||||||
f.read(skip)
|
|
||||||
code = marshal.load(f)
|
|
||||||
f.close()
|
|
||||||
safe = True
|
|
||||||
symbols = dict.fromkeys(iter_symbols(code))
|
|
||||||
for bad in ['__file__', '__path__']:
|
|
||||||
if bad in symbols:
|
|
||||||
log.warn("%s: module references %s", module, bad)
|
|
||||||
safe = False
|
|
||||||
if 'inspect' in symbols:
|
|
||||||
for bad in [
|
|
||||||
'getsource', 'getabsfile', 'getsourcefile', 'getfile'
|
|
||||||
'getsourcelines', 'findsource', 'getcomments', 'getframeinfo',
|
|
||||||
'getinnerframes', 'getouterframes', 'stack', 'trace'
|
|
||||||
]:
|
|
||||||
if bad in symbols:
|
|
||||||
log.warn("%s: module MAY be using inspect.%s", module, bad)
|
|
||||||
safe = False
|
|
||||||
return safe
|
|
||||||
|
|
||||||
|
|
||||||
def iter_symbols(code):
|
|
||||||
"""Yield names and strings used by `code` and its nested code objects"""
|
|
||||||
for name in code.co_names:
|
|
||||||
yield name
|
|
||||||
for const in code.co_consts:
|
|
||||||
if isinstance(const, str):
|
|
||||||
yield const
|
|
||||||
elif isinstance(const, CodeType):
|
|
||||||
for name in iter_symbols(const):
|
|
||||||
yield name
|
|
||||||
|
|
||||||
|
|
||||||
def can_scan():
|
|
||||||
if not sys.platform.startswith('java') and sys.platform != 'cli':
|
|
||||||
# CPython, PyPy, etc.
|
|
||||||
return True
|
|
||||||
log.warn("Unable to analyze compiled code on this platform.")
|
|
||||||
log.warn("Please ask the author to include a 'zip_safe'"
|
|
||||||
" setting (either True or False) in the package's setup.py")
|
|
||||||
|
|
||||||
|
|
||||||
# Attribute names of options for commands that might need to be convinced to
|
|
||||||
# install to the egg build directory
|
|
||||||
|
|
||||||
INSTALL_DIRECTORY_ATTRS = [
|
|
||||||
'install_lib', 'install_dir', 'install_data', 'install_base'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True,
|
|
||||||
mode='w'):
|
|
||||||
"""Create a zip file from all the files under 'base_dir'. The output
|
|
||||||
zip file will be named 'base_dir' + ".zip". Uses either the "zipfile"
|
|
||||||
Python module (if available) or the InfoZIP "zip" utility (if installed
|
|
||||||
and found on the default search path). If neither tool is available,
|
|
||||||
raises DistutilsExecError. Returns the name of the output zip file.
|
|
||||||
"""
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
mkpath(os.path.dirname(zip_filename), dry_run=dry_run)
|
|
||||||
log.info("creating '%s' and adding '%s' to it", zip_filename, base_dir)
|
|
||||||
|
|
||||||
def visit(z, dirname, names):
|
|
||||||
for name in names:
|
|
||||||
path = os.path.normpath(os.path.join(dirname, name))
|
|
||||||
if os.path.isfile(path):
|
|
||||||
p = path[len(base_dir) + 1:]
|
|
||||||
if not dry_run:
|
|
||||||
z.write(path, p)
|
|
||||||
log.debug("adding '%s'", p)
|
|
||||||
|
|
||||||
compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
|
|
||||||
if not dry_run:
|
|
||||||
z = zipfile.ZipFile(zip_filename, mode, compression=compression)
|
|
||||||
for dirname, dirs, files in sorted_walk(base_dir):
|
|
||||||
visit(z, dirname, files)
|
|
||||||
z.close()
|
|
||||||
else:
|
|
||||||
for dirname, dirs, files in sorted_walk(base_dir):
|
|
||||||
visit(None, dirname, files)
|
|
||||||
return zip_filename
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import distutils.command.bdist_rpm as orig
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from setuptools import SetuptoolsDeprecationWarning
|
|
||||||
|
|
||||||
|
|
||||||
class bdist_rpm(orig.bdist_rpm):
|
|
||||||
"""
|
|
||||||
Override the default bdist_rpm behavior to do the following:
|
|
||||||
|
|
||||||
1. Run egg_info to ensure the name and version are properly calculated.
|
|
||||||
2. Always run 'install' using --single-version-externally-managed to
|
|
||||||
disable eggs in RPM distributions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
warnings.warn(
|
|
||||||
"bdist_rpm is deprecated and will be removed in a future "
|
|
||||||
"version. Use bdist_wheel (wheel packages) instead.",
|
|
||||||
SetuptoolsDeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ensure distro name is up-to-date
|
|
||||||
self.run_command('egg_info')
|
|
||||||
|
|
||||||
orig.bdist_rpm.run(self)
|
|
||||||
|
|
||||||
def _make_spec_file(self):
|
|
||||||
spec = orig.bdist_rpm._make_spec_file(self)
|
|
||||||
spec = [
|
|
||||||
line.replace(
|
|
||||||
"setup.py install ",
|
|
||||||
"setup.py install --single-version-externally-managed "
|
|
||||||
).replace(
|
|
||||||
"%setup",
|
|
||||||
"%setup -n %{name}-%{unmangled_version}"
|
|
||||||
)
|
|
||||||
for line in spec
|
|
||||||
]
|
|
||||||
return spec
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import sys
|
|
||||||
import warnings
|
|
||||||
from typing import TYPE_CHECKING, List, Dict
|
|
||||||
from distutils.command.build import build as _build
|
|
||||||
|
|
||||||
from setuptools import SetuptoolsDeprecationWarning
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
from typing import Protocol
|
|
||||||
elif TYPE_CHECKING:
|
|
||||||
from typing_extensions import Protocol
|
|
||||||
else:
|
|
||||||
from abc import ABC as Protocol
|
|
||||||
|
|
||||||
|
|
||||||
_ORIGINAL_SUBCOMMANDS = {"build_py", "build_clib", "build_ext", "build_scripts"}
|
|
||||||
|
|
||||||
|
|
||||||
class build(_build):
|
|
||||||
# copy to avoid sharing the object with parent class
|
|
||||||
sub_commands = _build.sub_commands[:]
|
|
||||||
|
|
||||||
def get_sub_commands(self):
|
|
||||||
subcommands = {cmd[0] for cmd in _build.sub_commands}
|
|
||||||
if subcommands - _ORIGINAL_SUBCOMMANDS:
|
|
||||||
msg = """
|
|
||||||
It seems that you are using `distutils.command.build` to add
|
|
||||||
new subcommands. Using `distutils` directly is considered deprecated,
|
|
||||||
please use `setuptools.command.build`.
|
|
||||||
"""
|
|
||||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
|
||||||
self.sub_commands = _build.sub_commands
|
|
||||||
return super().get_sub_commands()
|
|
||||||
|
|
||||||
|
|
||||||
class SubCommand(Protocol):
|
|
||||||
"""In order to support editable installations (see :pep:`660`) all
|
|
||||||
build subcommands **SHOULD** implement this protocol. They also **MUST** inherit
|
|
||||||
from ``setuptools.Command``.
|
|
||||||
|
|
||||||
When creating an :pep:`editable wheel <660>`, ``setuptools`` will try to evaluate
|
|
||||||
custom ``build`` subcommands using the following procedure:
|
|
||||||
|
|
||||||
1. ``setuptools`` will set the ``editable_mode`` attribute to ``True``
|
|
||||||
2. ``setuptools`` will execute the ``run()`` command.
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
Subcommands **SHOULD** take advantage of ``editable_mode=True`` to adequate
|
|
||||||
its behaviour or perform optimisations.
|
|
||||||
|
|
||||||
For example, if a subcommand doesn't need to generate an extra file and
|
|
||||||
all it does is to copy a source file into the build directory,
|
|
||||||
``run()`` **SHOULD** simply "early return".
|
|
||||||
|
|
||||||
Similarly, if the subcommand creates files that would be placed alongside
|
|
||||||
Python files in the final distribution, during an editable install
|
|
||||||
the command **SHOULD** generate these files "in place" (i.e. write them to
|
|
||||||
the original source directory, instead of using the build directory).
|
|
||||||
Note that ``get_output_mapping()`` should reflect that and include mappings
|
|
||||||
for "in place" builds accordingly.
|
|
||||||
|
|
||||||
3. ``setuptools`` use any knowledge it can derive from the return values of
|
|
||||||
``get_outputs()`` and ``get_output_mapping()`` to create an editable wheel.
|
|
||||||
When relevant ``setuptools`` **MAY** attempt to use file links based on the value
|
|
||||||
of ``get_output_mapping()``. Alternatively, ``setuptools`` **MAY** attempt to use
|
|
||||||
:doc:`import hooks <python:reference/import>` to redirect any attempt to import
|
|
||||||
to the directory with the original source code and other files built in place.
|
|
||||||
|
|
||||||
Please note that custom sub-commands **SHOULD NOT** rely on ``run()`` being
|
|
||||||
executed (or not) to provide correct return values for ``get_outputs()``,
|
|
||||||
``get_output_mapping()`` or ``get_source_files()``. The ``get_*`` methods should
|
|
||||||
work independently of ``run()``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
editable_mode: bool = False
|
|
||||||
"""Boolean flag that will be set to ``True`` when setuptools is used for an
|
|
||||||
editable installation (see :pep:`660`).
|
|
||||||
Implementations **SHOULD** explicitly set the default value of this attribute to
|
|
||||||
``False``.
|
|
||||||
When subcommands run, they can use this flag to perform optimizations or change
|
|
||||||
their behaviour accordingly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
build_lib: str
|
|
||||||
"""String representing the directory where the build artifacts should be stored,
|
|
||||||
e.g. ``build/lib``.
|
|
||||||
For example, if a distribution wants to provide a Python module named ``pkg.mod``,
|
|
||||||
then a corresponding file should be written to ``{build_lib}/package/module.py``.
|
|
||||||
A way of thinking about this is that the files saved under ``build_lib``
|
|
||||||
would be eventually copied to one of the directories in :obj:`site.PREFIXES`
|
|
||||||
upon installation.
|
|
||||||
|
|
||||||
A command that produces platform-independent files (e.g. compiling text templates
|
|
||||||
into Python functions), **CAN** initialize ``build_lib`` by copying its value from
|
|
||||||
the ``build_py`` command. On the other hand, a command that produces
|
|
||||||
platform-specific files **CAN** initialize ``build_lib`` by copying its value from
|
|
||||||
the ``build_ext`` command. In general this is done inside the ``finalize_options``
|
|
||||||
method with the help of the ``set_undefined_options`` command::
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
self.set_undefined_options("build_py", ("build_lib", "build_lib"))
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
"""(Required by the original :class:`setuptools.Command` interface)"""
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
"""(Required by the original :class:`setuptools.Command` interface)"""
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""(Required by the original :class:`setuptools.Command` interface)"""
|
|
||||||
|
|
||||||
def get_source_files(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Return a list of all files that are used by the command to create the expected
|
|
||||||
outputs.
|
|
||||||
For example, if your build command transpiles Java files into Python, you should
|
|
||||||
list here all the Java files.
|
|
||||||
The primary purpose of this function is to help populating the ``sdist``
|
|
||||||
with all the files necessary to build the distribution.
|
|
||||||
All files should be strings relative to the project root directory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_outputs(self) -> List[str]:
|
|
||||||
"""
|
|
||||||
Return a list of files intended for distribution as they would have been
|
|
||||||
produced by the build.
|
|
||||||
These files should be strings in the form of
|
|
||||||
``"{build_lib}/destination/file/path"``.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
The return value of ``get_output()`` should include all files used as keys
|
|
||||||
in ``get_output_mapping()`` plus files that are generated during the build
|
|
||||||
and don't correspond to any source file already present in the project.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_output_mapping(self) -> Dict[str, str]:
|
|
||||||
"""
|
|
||||||
Return a mapping between destination files as they would be produced by the
|
|
||||||
build (dict keys) into the respective existing (source) files (dict values).
|
|
||||||
Existing (source) files should be represented as strings relative to the project
|
|
||||||
root directory.
|
|
||||||
Destination files should be strings in the form of
|
|
||||||
``"{build_lib}/destination/file/path"``.
|
|
||||||
"""
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import distutils.command.build_clib as orig
|
|
||||||
from distutils.errors import DistutilsSetupError
|
|
||||||
from distutils import log
|
|
||||||
from setuptools.dep_util import newer_pairwise_group
|
|
||||||
|
|
||||||
|
|
||||||
class build_clib(orig.build_clib):
|
|
||||||
"""
|
|
||||||
Override the default build_clib behaviour to do the following:
|
|
||||||
|
|
||||||
1. Implement a rudimentary timestamp-based dependency system
|
|
||||||
so 'compile()' doesn't run every time.
|
|
||||||
2. Add more keys to the 'build_info' dictionary:
|
|
||||||
* obj_deps - specify dependencies for each object compiled.
|
|
||||||
this should be a dictionary mapping a key
|
|
||||||
with the source filename to a list of
|
|
||||||
dependencies. Use an empty string for global
|
|
||||||
dependencies.
|
|
||||||
* cflags - specify a list of additional flags to pass to
|
|
||||||
the compiler.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_libraries(self, libraries):
|
|
||||||
for (lib_name, build_info) in libraries:
|
|
||||||
sources = build_info.get('sources')
|
|
||||||
if sources is None or not isinstance(sources, (list, tuple)):
|
|
||||||
raise DistutilsSetupError(
|
|
||||||
"in 'libraries' option (library '%s'), "
|
|
||||||
"'sources' must be present and must be "
|
|
||||||
"a list of source filenames" % lib_name)
|
|
||||||
sources = sorted(list(sources))
|
|
||||||
|
|
||||||
log.info("building '%s' library", lib_name)
|
|
||||||
|
|
||||||
# Make sure everything is the correct type.
|
|
||||||
# obj_deps should be a dictionary of keys as sources
|
|
||||||
# and a list/tuple of files that are its dependencies.
|
|
||||||
obj_deps = build_info.get('obj_deps', dict())
|
|
||||||
if not isinstance(obj_deps, dict):
|
|
||||||
raise DistutilsSetupError(
|
|
||||||
"in 'libraries' option (library '%s'), "
|
|
||||||
"'obj_deps' must be a dictionary of "
|
|
||||||
"type 'source: list'" % lib_name)
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
# Get the global dependencies that are specified by the '' key.
|
|
||||||
# These will go into every source's dependency list.
|
|
||||||
global_deps = obj_deps.get('', list())
|
|
||||||
if not isinstance(global_deps, (list, tuple)):
|
|
||||||
raise DistutilsSetupError(
|
|
||||||
"in 'libraries' option (library '%s'), "
|
|
||||||
"'obj_deps' must be a dictionary of "
|
|
||||||
"type 'source: list'" % lib_name)
|
|
||||||
|
|
||||||
# Build the list to be used by newer_pairwise_group
|
|
||||||
# each source will be auto-added to its dependencies.
|
|
||||||
for source in sources:
|
|
||||||
src_deps = [source]
|
|
||||||
src_deps.extend(global_deps)
|
|
||||||
extra_deps = obj_deps.get(source, list())
|
|
||||||
if not isinstance(extra_deps, (list, tuple)):
|
|
||||||
raise DistutilsSetupError(
|
|
||||||
"in 'libraries' option (library '%s'), "
|
|
||||||
"'obj_deps' must be a dictionary of "
|
|
||||||
"type 'source: list'" % lib_name)
|
|
||||||
src_deps.extend(extra_deps)
|
|
||||||
dependencies.append(src_deps)
|
|
||||||
|
|
||||||
expected_objects = self.compiler.object_filenames(
|
|
||||||
sources,
|
|
||||||
output_dir=self.build_temp,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
newer_pairwise_group(dependencies, expected_objects)
|
|
||||||
!= ([], [])
|
|
||||||
):
|
|
||||||
# First, compile the source code to object files in the library
|
|
||||||
# directory. (This should probably change to putting object
|
|
||||||
# files in a temporary build directory.)
|
|
||||||
macros = build_info.get('macros')
|
|
||||||
include_dirs = build_info.get('include_dirs')
|
|
||||||
cflags = build_info.get('cflags')
|
|
||||||
self.compiler.compile(
|
|
||||||
sources,
|
|
||||||
output_dir=self.build_temp,
|
|
||||||
macros=macros,
|
|
||||||
include_dirs=include_dirs,
|
|
||||||
extra_postargs=cflags,
|
|
||||||
debug=self.debug
|
|
||||||
)
|
|
||||||
|
|
||||||
# Now "link" the object files together into a static library.
|
|
||||||
# (On Unix at least, this isn't really linking -- it just
|
|
||||||
# builds an archive. Whatever.)
|
|
||||||
self.compiler.create_static_lib(
|
|
||||||
expected_objects,
|
|
||||||
lib_name,
|
|
||||||
output_dir=self.build_clib,
|
|
||||||
debug=self.debug
|
|
||||||
)
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
import itertools
|
|
||||||
from importlib.machinery import EXTENSION_SUFFIXES
|
|
||||||
from importlib.util import cache_from_source as _compiled_file_name
|
|
||||||
from typing import Dict, Iterator, List, Tuple
|
|
||||||
|
|
||||||
from distutils.command.build_ext import build_ext as _du_build_ext
|
|
||||||
from distutils.ccompiler import new_compiler
|
|
||||||
from distutils.sysconfig import customize_compiler, get_config_var
|
|
||||||
from distutils import log
|
|
||||||
|
|
||||||
from setuptools.errors import BaseError
|
|
||||||
from setuptools.extension import Extension, Library
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Attempt to use Cython for building extensions, if available
|
|
||||||
from Cython.Distutils.build_ext import build_ext as _build_ext
|
|
||||||
# Additionally, assert that the compiler module will load
|
|
||||||
# also. Ref #1229.
|
|
||||||
__import__('Cython.Compiler.Main')
|
|
||||||
except ImportError:
|
|
||||||
_build_ext = _du_build_ext
|
|
||||||
|
|
||||||
# make sure _config_vars is initialized
|
|
||||||
get_config_var("LDSHARED")
|
|
||||||
from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def _customize_compiler_for_shlib(compiler):
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
# building .dylib requires additional compiler flags on OSX; here we
|
|
||||||
# temporarily substitute the pyconfig.h variables so that distutils'
|
|
||||||
# 'customize_compiler' uses them before we build the shared libraries.
|
|
||||||
tmp = _CONFIG_VARS.copy()
|
|
||||||
try:
|
|
||||||
# XXX Help! I don't have any idea whether these are right...
|
|
||||||
_CONFIG_VARS['LDSHARED'] = (
|
|
||||||
"gcc -Wl,-x -dynamiclib -undefined dynamic_lookup")
|
|
||||||
_CONFIG_VARS['CCSHARED'] = " -dynamiclib"
|
|
||||||
_CONFIG_VARS['SO'] = ".dylib"
|
|
||||||
customize_compiler(compiler)
|
|
||||||
finally:
|
|
||||||
_CONFIG_VARS.clear()
|
|
||||||
_CONFIG_VARS.update(tmp)
|
|
||||||
else:
|
|
||||||
customize_compiler(compiler)
|
|
||||||
|
|
||||||
|
|
||||||
have_rtld = False
|
|
||||||
use_stubs = False
|
|
||||||
libtype = 'shared'
|
|
||||||
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
use_stubs = True
|
|
||||||
elif os.name != 'nt':
|
|
||||||
try:
|
|
||||||
import dl
|
|
||||||
use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW')
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def if_dl(s):
|
|
||||||
return s if have_rtld else ''
|
|
||||||
|
|
||||||
|
|
||||||
def get_abi3_suffix():
|
|
||||||
"""Return the file extension for an abi3-compliant Extension()"""
|
|
||||||
for suffix in EXTENSION_SUFFIXES:
|
|
||||||
if '.abi3' in suffix: # Unix
|
|
||||||
return suffix
|
|
||||||
elif suffix == '.pyd': # Windows
|
|
||||||
return suffix
|
|
||||||
|
|
||||||
|
|
||||||
class build_ext(_build_ext):
|
|
||||||
editable_mode: bool = False
|
|
||||||
inplace: bool = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Build extensions in build directory, then copy if --inplace"""
|
|
||||||
old_inplace, self.inplace = self.inplace, 0
|
|
||||||
_build_ext.run(self)
|
|
||||||
self.inplace = old_inplace
|
|
||||||
if old_inplace:
|
|
||||||
self.copy_extensions_to_source()
|
|
||||||
|
|
||||||
def _get_inplace_equivalent(self, build_py, ext: Extension) -> Tuple[str, str]:
|
|
||||||
fullname = self.get_ext_fullname(ext.name)
|
|
||||||
filename = self.get_ext_filename(fullname)
|
|
||||||
modpath = fullname.split('.')
|
|
||||||
package = '.'.join(modpath[:-1])
|
|
||||||
package_dir = build_py.get_package_dir(package)
|
|
||||||
inplace_file = os.path.join(package_dir, os.path.basename(filename))
|
|
||||||
regular_file = os.path.join(self.build_lib, filename)
|
|
||||||
return (inplace_file, regular_file)
|
|
||||||
|
|
||||||
def copy_extensions_to_source(self):
|
|
||||||
build_py = self.get_finalized_command('build_py')
|
|
||||||
for ext in self.extensions:
|
|
||||||
inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)
|
|
||||||
|
|
||||||
# Always copy, even if source is older than destination, to ensure
|
|
||||||
# that the right extensions for the current Python/platform are
|
|
||||||
# used.
|
|
||||||
if os.path.exists(regular_file) or not ext.optional:
|
|
||||||
self.copy_file(regular_file, inplace_file, level=self.verbose)
|
|
||||||
|
|
||||||
if ext._needs_stub:
|
|
||||||
inplace_stub = self._get_equivalent_stub(ext, inplace_file)
|
|
||||||
self._write_stub_file(inplace_stub, ext, compile=True)
|
|
||||||
# Always compile stub and remove the original (leave the cache behind)
|
|
||||||
# (this behaviour was observed in previous iterations of the code)
|
|
||||||
|
|
||||||
def _get_equivalent_stub(self, ext: Extension, output_file: str) -> str:
|
|
||||||
dir_ = os.path.dirname(output_file)
|
|
||||||
_, _, name = ext.name.rpartition(".")
|
|
||||||
return f"{os.path.join(dir_, name)}.py"
|
|
||||||
|
|
||||||
def _get_output_mapping(self) -> Iterator[Tuple[str, str]]:
|
|
||||||
if not self.inplace:
|
|
||||||
return
|
|
||||||
|
|
||||||
build_py = self.get_finalized_command('build_py')
|
|
||||||
opt = self.get_finalized_command('install_lib').optimize or ""
|
|
||||||
|
|
||||||
for ext in self.extensions:
|
|
||||||
inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext)
|
|
||||||
yield (regular_file, inplace_file)
|
|
||||||
|
|
||||||
if ext._needs_stub:
|
|
||||||
# This version of `build_ext` always builds artifacts in another dir,
|
|
||||||
# when "inplace=True" is given it just copies them back.
|
|
||||||
# This is done in the `copy_extensions_to_source` function, which
|
|
||||||
# always compile stub files via `_compile_and_remove_stub`.
|
|
||||||
# At the end of the process, a `.pyc` stub file is created without the
|
|
||||||
# corresponding `.py`.
|
|
||||||
|
|
||||||
inplace_stub = self._get_equivalent_stub(ext, inplace_file)
|
|
||||||
regular_stub = self._get_equivalent_stub(ext, regular_file)
|
|
||||||
inplace_cache = _compiled_file_name(inplace_stub, optimization=opt)
|
|
||||||
output_cache = _compiled_file_name(regular_stub, optimization=opt)
|
|
||||||
yield (output_cache, inplace_cache)
|
|
||||||
|
|
||||||
def get_ext_filename(self, fullname):
|
|
||||||
so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX')
|
|
||||||
if so_ext:
|
|
||||||
filename = os.path.join(*fullname.split('.')) + so_ext
|
|
||||||
else:
|
|
||||||
filename = _build_ext.get_ext_filename(self, fullname)
|
|
||||||
so_ext = get_config_var('EXT_SUFFIX')
|
|
||||||
|
|
||||||
if fullname in self.ext_map:
|
|
||||||
ext = self.ext_map[fullname]
|
|
||||||
use_abi3 = getattr(ext, 'py_limited_api') and get_abi3_suffix()
|
|
||||||
if use_abi3:
|
|
||||||
filename = filename[:-len(so_ext)]
|
|
||||||
so_ext = get_abi3_suffix()
|
|
||||||
filename = filename + so_ext
|
|
||||||
if isinstance(ext, Library):
|
|
||||||
fn, ext = os.path.splitext(filename)
|
|
||||||
return self.shlib_compiler.library_filename(fn, libtype)
|
|
||||||
elif use_stubs and ext._links_to_dynamic:
|
|
||||||
d, fn = os.path.split(filename)
|
|
||||||
return os.path.join(d, 'dl-' + fn)
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
_build_ext.initialize_options(self)
|
|
||||||
self.shlib_compiler = None
|
|
||||||
self.shlibs = []
|
|
||||||
self.ext_map = {}
|
|
||||||
self.editable_mode = False
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
_build_ext.finalize_options(self)
|
|
||||||
self.extensions = self.extensions or []
|
|
||||||
self.check_extensions_list(self.extensions)
|
|
||||||
self.shlibs = [ext for ext in self.extensions
|
|
||||||
if isinstance(ext, Library)]
|
|
||||||
if self.shlibs:
|
|
||||||
self.setup_shlib_compiler()
|
|
||||||
for ext in self.extensions:
|
|
||||||
ext._full_name = self.get_ext_fullname(ext.name)
|
|
||||||
for ext in self.extensions:
|
|
||||||
fullname = ext._full_name
|
|
||||||
self.ext_map[fullname] = ext
|
|
||||||
|
|
||||||
# distutils 3.1 will also ask for module names
|
|
||||||
# XXX what to do with conflicts?
|
|
||||||
self.ext_map[fullname.split('.')[-1]] = ext
|
|
||||||
|
|
||||||
ltd = self.shlibs and self.links_to_dynamic(ext) or False
|
|
||||||
ns = ltd and use_stubs and not isinstance(ext, Library)
|
|
||||||
ext._links_to_dynamic = ltd
|
|
||||||
ext._needs_stub = ns
|
|
||||||
filename = ext._file_name = self.get_ext_filename(fullname)
|
|
||||||
libdir = os.path.dirname(os.path.join(self.build_lib, filename))
|
|
||||||
if ltd and libdir not in ext.library_dirs:
|
|
||||||
ext.library_dirs.append(libdir)
|
|
||||||
if ltd and use_stubs and os.curdir not in ext.runtime_library_dirs:
|
|
||||||
ext.runtime_library_dirs.append(os.curdir)
|
|
||||||
|
|
||||||
if self.editable_mode:
|
|
||||||
self.inplace = True
|
|
||||||
|
|
||||||
def setup_shlib_compiler(self):
|
|
||||||
compiler = self.shlib_compiler = new_compiler(
|
|
||||||
compiler=self.compiler, dry_run=self.dry_run, force=self.force
|
|
||||||
)
|
|
||||||
_customize_compiler_for_shlib(compiler)
|
|
||||||
|
|
||||||
if self.include_dirs is not None:
|
|
||||||
compiler.set_include_dirs(self.include_dirs)
|
|
||||||
if self.define is not None:
|
|
||||||
# 'define' option is a list of (name,value) tuples
|
|
||||||
for (name, value) in self.define:
|
|
||||||
compiler.define_macro(name, value)
|
|
||||||
if self.undef is not None:
|
|
||||||
for macro in self.undef:
|
|
||||||
compiler.undefine_macro(macro)
|
|
||||||
if self.libraries is not None:
|
|
||||||
compiler.set_libraries(self.libraries)
|
|
||||||
if self.library_dirs is not None:
|
|
||||||
compiler.set_library_dirs(self.library_dirs)
|
|
||||||
if self.rpath is not None:
|
|
||||||
compiler.set_runtime_library_dirs(self.rpath)
|
|
||||||
if self.link_objects is not None:
|
|
||||||
compiler.set_link_objects(self.link_objects)
|
|
||||||
|
|
||||||
# hack so distutils' build_extension() builds a library instead
|
|
||||||
compiler.link_shared_object = link_shared_object.__get__(compiler)
|
|
||||||
|
|
||||||
def get_export_symbols(self, ext):
|
|
||||||
if isinstance(ext, Library):
|
|
||||||
return ext.export_symbols
|
|
||||||
return _build_ext.get_export_symbols(self, ext)
|
|
||||||
|
|
||||||
def build_extension(self, ext):
|
|
||||||
ext._convert_pyx_sources_to_lang()
|
|
||||||
_compiler = self.compiler
|
|
||||||
try:
|
|
||||||
if isinstance(ext, Library):
|
|
||||||
self.compiler = self.shlib_compiler
|
|
||||||
_build_ext.build_extension(self, ext)
|
|
||||||
if ext._needs_stub:
|
|
||||||
build_lib = self.get_finalized_command('build_py').build_lib
|
|
||||||
self.write_stub(build_lib, ext)
|
|
||||||
finally:
|
|
||||||
self.compiler = _compiler
|
|
||||||
|
|
||||||
def links_to_dynamic(self, ext):
|
|
||||||
"""Return true if 'ext' links to a dynamic lib in the same package"""
|
|
||||||
# XXX this should check to ensure the lib is actually being built
|
|
||||||
# XXX as dynamic, and not just using a locally-found version or a
|
|
||||||
# XXX static-compiled version
|
|
||||||
libnames = dict.fromkeys([lib._full_name for lib in self.shlibs])
|
|
||||||
pkg = '.'.join(ext._full_name.split('.')[:-1] + [''])
|
|
||||||
return any(pkg + libname in libnames for libname in ext.libraries)
|
|
||||||
|
|
||||||
def get_outputs(self) -> List[str]:
|
|
||||||
if self.inplace:
|
|
||||||
return list(self.get_output_mapping().keys())
|
|
||||||
return sorted(_build_ext.get_outputs(self) + self.__get_stubs_outputs())
|
|
||||||
|
|
||||||
def get_output_mapping(self) -> Dict[str, str]:
|
|
||||||
"""See :class:`setuptools.commands.build.SubCommand`"""
|
|
||||||
mapping = self._get_output_mapping()
|
|
||||||
return dict(sorted(mapping, key=lambda x: x[0]))
|
|
||||||
|
|
||||||
def __get_stubs_outputs(self):
|
|
||||||
# assemble the base name for each extension that needs a stub
|
|
||||||
ns_ext_bases = (
|
|
||||||
os.path.join(self.build_lib, *ext._full_name.split('.'))
|
|
||||||
for ext in self.extensions
|
|
||||||
if ext._needs_stub
|
|
||||||
)
|
|
||||||
# pair each base with the extension
|
|
||||||
pairs = itertools.product(ns_ext_bases, self.__get_output_extensions())
|
|
||||||
return list(base + fnext for base, fnext in pairs)
|
|
||||||
|
|
||||||
def __get_output_extensions(self):
|
|
||||||
yield '.py'
|
|
||||||
yield '.pyc'
|
|
||||||
if self.get_finalized_command('build_py').optimize:
|
|
||||||
yield '.pyo'
|
|
||||||
|
|
||||||
def write_stub(self, output_dir, ext, compile=False):
|
|
||||||
stub_file = os.path.join(output_dir, *ext._full_name.split('.')) + '.py'
|
|
||||||
self._write_stub_file(stub_file, ext, compile)
|
|
||||||
|
|
||||||
def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
|
|
||||||
log.info("writing stub loader for %s to %s", ext._full_name, stub_file)
|
|
||||||
if compile and os.path.exists(stub_file):
|
|
||||||
raise BaseError(stub_file + " already exists! Please delete.")
|
|
||||||
if not self.dry_run:
|
|
||||||
f = open(stub_file, 'w')
|
|
||||||
f.write(
|
|
||||||
'\n'.join([
|
|
||||||
"def __bootstrap__():",
|
|
||||||
" global __bootstrap__, __file__, __loader__",
|
|
||||||
" import sys, os, pkg_resources, importlib.util" +
|
|
||||||
if_dl(", dl"),
|
|
||||||
" __file__ = pkg_resources.resource_filename"
|
|
||||||
"(__name__,%r)"
|
|
||||||
% os.path.basename(ext._file_name),
|
|
||||||
" del __bootstrap__",
|
|
||||||
" if '__loader__' in globals():",
|
|
||||||
" del __loader__",
|
|
||||||
if_dl(" old_flags = sys.getdlopenflags()"),
|
|
||||||
" old_dir = os.getcwd()",
|
|
||||||
" try:",
|
|
||||||
" os.chdir(os.path.dirname(__file__))",
|
|
||||||
if_dl(" sys.setdlopenflags(dl.RTLD_NOW)"),
|
|
||||||
" spec = importlib.util.spec_from_file_location(",
|
|
||||||
" __name__, __file__)",
|
|
||||||
" mod = importlib.util.module_from_spec(spec)",
|
|
||||||
" spec.loader.exec_module(mod)",
|
|
||||||
" finally:",
|
|
||||||
if_dl(" sys.setdlopenflags(old_flags)"),
|
|
||||||
" os.chdir(old_dir)",
|
|
||||||
"__bootstrap__()",
|
|
||||||
"" # terminal \n
|
|
||||||
])
|
|
||||||
)
|
|
||||||
f.close()
|
|
||||||
if compile:
|
|
||||||
self._compile_and_remove_stub(stub_file)
|
|
||||||
|
|
||||||
def _compile_and_remove_stub(self, stub_file: str):
|
|
||||||
from distutils.util import byte_compile
|
|
||||||
|
|
||||||
byte_compile([stub_file], optimize=0,
|
|
||||||
force=True, dry_run=self.dry_run)
|
|
||||||
optimize = self.get_finalized_command('install_lib').optimize
|
|
||||||
if optimize > 0:
|
|
||||||
byte_compile([stub_file], optimize=optimize,
|
|
||||||
force=True, dry_run=self.dry_run)
|
|
||||||
if os.path.exists(stub_file) and not self.dry_run:
|
|
||||||
os.unlink(stub_file)
|
|
||||||
|
|
||||||
|
|
||||||
if use_stubs or os.name == 'nt':
|
|
||||||
# Build shared libraries
|
|
||||||
#
|
|
||||||
def link_shared_object(
|
|
||||||
self, objects, output_libname, output_dir=None, libraries=None,
|
|
||||||
library_dirs=None, runtime_library_dirs=None, export_symbols=None,
|
|
||||||
debug=0, extra_preargs=None, extra_postargs=None, build_temp=None,
|
|
||||||
target_lang=None):
|
|
||||||
self.link(
|
|
||||||
self.SHARED_LIBRARY, objects, output_libname,
|
|
||||||
output_dir, libraries, library_dirs, runtime_library_dirs,
|
|
||||||
export_symbols, debug, extra_preargs, extra_postargs,
|
|
||||||
build_temp, target_lang
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Build static libraries everywhere else
|
|
||||||
libtype = 'static'
|
|
||||||
|
|
||||||
def link_shared_object(
|
|
||||||
self, objects, output_libname, output_dir=None, libraries=None,
|
|
||||||
library_dirs=None, runtime_library_dirs=None, export_symbols=None,
|
|
||||||
debug=0, extra_preargs=None, extra_postargs=None, build_temp=None,
|
|
||||||
target_lang=None):
|
|
||||||
# XXX we need to either disallow these attrs on Library instances,
|
|
||||||
# or warn/abort here if set, or something...
|
|
||||||
# libraries=None, library_dirs=None, runtime_library_dirs=None,
|
|
||||||
# export_symbols=None, extra_preargs=None, extra_postargs=None,
|
|
||||||
# build_temp=None
|
|
||||||
|
|
||||||
assert output_dir is None # distutils build_ext doesn't pass this
|
|
||||||
output_dir, filename = os.path.split(output_libname)
|
|
||||||
basename, ext = os.path.splitext(filename)
|
|
||||||
if self.library_filename("x").startswith('lib'):
|
|
||||||
# strip 'lib' prefix; this is kludgy if some platform uses
|
|
||||||
# a different prefix
|
|
||||||
basename = basename[3:]
|
|
||||||
|
|
||||||
self.create_static_lib(
|
|
||||||
objects, basename, output_dir, debug, target_lang
|
|
||||||
)
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
from functools import partial
|
|
||||||
from glob import glob
|
|
||||||
from distutils.util import convert_path
|
|
||||||
import distutils.command.build_py as orig
|
|
||||||
import os
|
|
||||||
import fnmatch
|
|
||||||
import textwrap
|
|
||||||
import io
|
|
||||||
import distutils.errors
|
|
||||||
import itertools
|
|
||||||
import stat
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, Iterable, Iterator, List, Optional, Tuple
|
|
||||||
|
|
||||||
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
|
|
||||||
from setuptools.extern.more_itertools import unique_everseen
|
|
||||||
|
|
||||||
|
|
||||||
def make_writable(target):
|
|
||||||
os.chmod(target, os.stat(target).st_mode | stat.S_IWRITE)
|
|
||||||
|
|
||||||
|
|
||||||
class build_py(orig.build_py):
|
|
||||||
"""Enhanced 'build_py' command that includes data files with packages
|
|
||||||
|
|
||||||
The data files are specified via a 'package_data' argument to 'setup()'.
|
|
||||||
See 'setuptools.dist.Distribution' for more details.
|
|
||||||
|
|
||||||
Also, this version of the 'build_py' command allows you to specify both
|
|
||||||
'py_modules' and 'packages' in the same setup operation.
|
|
||||||
"""
|
|
||||||
editable_mode: bool = False
|
|
||||||
existing_egg_info_dir: Optional[str] = None #: Private API, internal use only.
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
orig.build_py.finalize_options(self)
|
|
||||||
self.package_data = self.distribution.package_data
|
|
||||||
self.exclude_package_data = self.distribution.exclude_package_data or {}
|
|
||||||
if 'data_files' in self.__dict__:
|
|
||||||
del self.__dict__['data_files']
|
|
||||||
self.__updated_files = []
|
|
||||||
|
|
||||||
def copy_file(self, infile, outfile, preserve_mode=1, preserve_times=1,
|
|
||||||
link=None, level=1):
|
|
||||||
# Overwrite base class to allow using links
|
|
||||||
if link:
|
|
||||||
infile = str(Path(infile).resolve())
|
|
||||||
outfile = str(Path(outfile).resolve())
|
|
||||||
return super().copy_file(infile, outfile, preserve_mode, preserve_times,
|
|
||||||
link, level)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Build modules, packages, and copy data files to build directory"""
|
|
||||||
if not (self.py_modules or self.packages) or self.editable_mode:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.py_modules:
|
|
||||||
self.build_modules()
|
|
||||||
|
|
||||||
if self.packages:
|
|
||||||
self.build_packages()
|
|
||||||
self.build_package_data()
|
|
||||||
|
|
||||||
# Only compile actual .py files, using our base class' idea of what our
|
|
||||||
# output files are.
|
|
||||||
self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0))
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
"lazily compute data files"
|
|
||||||
if attr == 'data_files':
|
|
||||||
self.data_files = self._get_data_files()
|
|
||||||
return self.data_files
|
|
||||||
return orig.build_py.__getattr__(self, attr)
|
|
||||||
|
|
||||||
def build_module(self, module, module_file, package):
|
|
||||||
outfile, copied = orig.build_py.build_module(self, module, module_file, package)
|
|
||||||
if copied:
|
|
||||||
self.__updated_files.append(outfile)
|
|
||||||
return outfile, copied
|
|
||||||
|
|
||||||
def _get_data_files(self):
|
|
||||||
"""Generate list of '(package,src_dir,build_dir,filenames)' tuples"""
|
|
||||||
self.analyze_manifest()
|
|
||||||
return list(map(self._get_pkg_data_files, self.packages or ()))
|
|
||||||
|
|
||||||
def get_data_files_without_manifest(self):
|
|
||||||
"""
|
|
||||||
Generate list of ``(package,src_dir,build_dir,filenames)`` tuples,
|
|
||||||
but without triggering any attempt to analyze or build the manifest.
|
|
||||||
"""
|
|
||||||
# Prevent eventual errors from unset `manifest_files`
|
|
||||||
# (that would otherwise be set by `analyze_manifest`)
|
|
||||||
self.__dict__.setdefault('manifest_files', {})
|
|
||||||
return list(map(self._get_pkg_data_files, self.packages or ()))
|
|
||||||
|
|
||||||
def _get_pkg_data_files(self, package):
|
|
||||||
# Locate package source directory
|
|
||||||
src_dir = self.get_package_dir(package)
|
|
||||||
|
|
||||||
# Compute package build directory
|
|
||||||
build_dir = os.path.join(*([self.build_lib] + package.split('.')))
|
|
||||||
|
|
||||||
# Strip directory from globbed filenames
|
|
||||||
filenames = [
|
|
||||||
os.path.relpath(file, src_dir)
|
|
||||||
for file in self.find_data_files(package, src_dir)
|
|
||||||
]
|
|
||||||
return package, src_dir, build_dir, filenames
|
|
||||||
|
|
||||||
def find_data_files(self, package, src_dir):
|
|
||||||
"""Return filenames for package's data files in 'src_dir'"""
|
|
||||||
patterns = self._get_platform_patterns(
|
|
||||||
self.package_data,
|
|
||||||
package,
|
|
||||||
src_dir,
|
|
||||||
)
|
|
||||||
globs_expanded = map(partial(glob, recursive=True), patterns)
|
|
||||||
# flatten the expanded globs into an iterable of matches
|
|
||||||
globs_matches = itertools.chain.from_iterable(globs_expanded)
|
|
||||||
glob_files = filter(os.path.isfile, globs_matches)
|
|
||||||
files = itertools.chain(
|
|
||||||
self.manifest_files.get(package, []),
|
|
||||||
glob_files,
|
|
||||||
)
|
|
||||||
return self.exclude_data_files(package, src_dir, files)
|
|
||||||
|
|
||||||
def get_outputs(self, include_bytecode=1) -> List[str]:
|
|
||||||
"""See :class:`setuptools.commands.build.SubCommand`"""
|
|
||||||
if self.editable_mode:
|
|
||||||
return list(self.get_output_mapping().keys())
|
|
||||||
return super().get_outputs(include_bytecode)
|
|
||||||
|
|
||||||
def get_output_mapping(self) -> Dict[str, str]:
|
|
||||||
"""See :class:`setuptools.commands.build.SubCommand`"""
|
|
||||||
mapping = itertools.chain(
|
|
||||||
self._get_package_data_output_mapping(),
|
|
||||||
self._get_module_mapping(),
|
|
||||||
)
|
|
||||||
return dict(sorted(mapping, key=lambda x: x[0]))
|
|
||||||
|
|
||||||
def _get_module_mapping(self) -> Iterator[Tuple[str, str]]:
|
|
||||||
"""Iterate over all modules producing (dest, src) pairs."""
|
|
||||||
for (package, module, module_file) in self.find_all_modules():
|
|
||||||
package = package.split('.')
|
|
||||||
filename = self.get_module_outfile(self.build_lib, package, module)
|
|
||||||
yield (filename, module_file)
|
|
||||||
|
|
||||||
def _get_package_data_output_mapping(self) -> Iterator[Tuple[str, str]]:
|
|
||||||
"""Iterate over package data producing (dest, src) pairs."""
|
|
||||||
for package, src_dir, build_dir, filenames in self.data_files:
|
|
||||||
for filename in filenames:
|
|
||||||
target = os.path.join(build_dir, filename)
|
|
||||||
srcfile = os.path.join(src_dir, filename)
|
|
||||||
yield (target, srcfile)
|
|
||||||
|
|
||||||
def build_package_data(self):
|
|
||||||
"""Copy data files into build directory"""
|
|
||||||
for target, srcfile in self._get_package_data_output_mapping():
|
|
||||||
self.mkpath(os.path.dirname(target))
|
|
||||||
_outf, _copied = self.copy_file(srcfile, target)
|
|
||||||
make_writable(target)
|
|
||||||
|
|
||||||
def analyze_manifest(self):
|
|
||||||
self.manifest_files = mf = {}
|
|
||||||
if not self.distribution.include_package_data:
|
|
||||||
return
|
|
||||||
src_dirs = {}
|
|
||||||
for package in self.packages or ():
|
|
||||||
# Locate package source directory
|
|
||||||
src_dirs[assert_relative(self.get_package_dir(package))] = package
|
|
||||||
|
|
||||||
if (
|
|
||||||
getattr(self, 'existing_egg_info_dir', None)
|
|
||||||
and Path(self.existing_egg_info_dir, "SOURCES.txt").exists()
|
|
||||||
):
|
|
||||||
egg_info_dir = self.existing_egg_info_dir
|
|
||||||
manifest = Path(egg_info_dir, "SOURCES.txt")
|
|
||||||
files = manifest.read_text(encoding="utf-8").splitlines()
|
|
||||||
else:
|
|
||||||
self.run_command('egg_info')
|
|
||||||
ei_cmd = self.get_finalized_command('egg_info')
|
|
||||||
egg_info_dir = ei_cmd.egg_info
|
|
||||||
files = ei_cmd.filelist.files
|
|
||||||
|
|
||||||
check = _IncludePackageDataAbuse()
|
|
||||||
for path in self._filter_build_files(files, egg_info_dir):
|
|
||||||
d, f = os.path.split(assert_relative(path))
|
|
||||||
prev = None
|
|
||||||
oldf = f
|
|
||||||
while d and d != prev and d not in src_dirs:
|
|
||||||
prev = d
|
|
||||||
d, df = os.path.split(d)
|
|
||||||
f = os.path.join(df, f)
|
|
||||||
if d in src_dirs:
|
|
||||||
if f == oldf:
|
|
||||||
if check.is_module(f):
|
|
||||||
continue # it's a module, not data
|
|
||||||
else:
|
|
||||||
importable = check.importable_subpackage(src_dirs[d], f)
|
|
||||||
if importable:
|
|
||||||
check.warn(importable)
|
|
||||||
mf.setdefault(src_dirs[d], []).append(path)
|
|
||||||
|
|
||||||
def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]:
|
|
||||||
"""
|
|
||||||
``build_meta`` may try to create egg_info outside of the project directory,
|
|
||||||
and this can be problematic for certain plugins (reported in issue #3500).
|
|
||||||
|
|
||||||
Extensions might also include between their sources files created on the
|
|
||||||
``build_lib`` and ``build_temp`` directories.
|
|
||||||
|
|
||||||
This function should filter this case of invalid files out.
|
|
||||||
"""
|
|
||||||
build = self.get_finalized_command("build")
|
|
||||||
build_dirs = (egg_info, self.build_lib, build.build_temp, build.build_base)
|
|
||||||
norm_dirs = [os.path.normpath(p) for p in build_dirs if p]
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
norm_path = os.path.normpath(file)
|
|
||||||
if not os.path.isabs(file) or all(d not in norm_path for d in norm_dirs):
|
|
||||||
yield file
|
|
||||||
|
|
||||||
def get_data_files(self):
|
|
||||||
pass # Lazily compute data files in _get_data_files() function.
|
|
||||||
|
|
||||||
def check_package(self, package, package_dir):
|
|
||||||
"""Check namespace packages' __init__ for declare_namespace"""
|
|
||||||
try:
|
|
||||||
return self.packages_checked[package]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
init_py = orig.build_py.check_package(self, package, package_dir)
|
|
||||||
self.packages_checked[package] = init_py
|
|
||||||
|
|
||||||
if not init_py or not self.distribution.namespace_packages:
|
|
||||||
return init_py
|
|
||||||
|
|
||||||
for pkg in self.distribution.namespace_packages:
|
|
||||||
if pkg == package or pkg.startswith(package + '.'):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return init_py
|
|
||||||
|
|
||||||
with io.open(init_py, 'rb') as f:
|
|
||||||
contents = f.read()
|
|
||||||
if b'declare_namespace' not in contents:
|
|
||||||
raise distutils.errors.DistutilsError(
|
|
||||||
"Namespace package problem: %s is a namespace package, but "
|
|
||||||
"its\n__init__.py does not call declare_namespace()! Please "
|
|
||||||
'fix it.\n(See the setuptools manual under '
|
|
||||||
'"Namespace Packages" for details.)\n"' % (package,)
|
|
||||||
)
|
|
||||||
return init_py
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.packages_checked = {}
|
|
||||||
orig.build_py.initialize_options(self)
|
|
||||||
self.editable_mode = False
|
|
||||||
self.existing_egg_info_dir = None
|
|
||||||
|
|
||||||
def get_package_dir(self, package):
|
|
||||||
res = orig.build_py.get_package_dir(self, package)
|
|
||||||
if self.distribution.src_root is not None:
|
|
||||||
return os.path.join(self.distribution.src_root, res)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def exclude_data_files(self, package, src_dir, files):
|
|
||||||
"""Filter filenames for package's data files in 'src_dir'"""
|
|
||||||
files = list(files)
|
|
||||||
patterns = self._get_platform_patterns(
|
|
||||||
self.exclude_package_data,
|
|
||||||
package,
|
|
||||||
src_dir,
|
|
||||||
)
|
|
||||||
match_groups = (fnmatch.filter(files, pattern) for pattern in patterns)
|
|
||||||
# flatten the groups of matches into an iterable of matches
|
|
||||||
matches = itertools.chain.from_iterable(match_groups)
|
|
||||||
bad = set(matches)
|
|
||||||
keepers = (fn for fn in files if fn not in bad)
|
|
||||||
# ditch dupes
|
|
||||||
return list(unique_everseen(keepers))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_platform_patterns(spec, package, src_dir):
|
|
||||||
"""
|
|
||||||
yield platform-specific path patterns (suitable for glob
|
|
||||||
or fn_match) from a glob-based spec (such as
|
|
||||||
self.package_data or self.exclude_package_data)
|
|
||||||
matching package in src_dir.
|
|
||||||
"""
|
|
||||||
raw_patterns = itertools.chain(
|
|
||||||
spec.get('', []),
|
|
||||||
spec.get(package, []),
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
# Each pattern has to be converted to a platform-specific path
|
|
||||||
os.path.join(src_dir, convert_path(pattern))
|
|
||||||
for pattern in raw_patterns
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def assert_relative(path):
|
|
||||||
if not os.path.isabs(path):
|
|
||||||
return path
|
|
||||||
from distutils.errors import DistutilsSetupError
|
|
||||||
|
|
||||||
msg = (
|
|
||||||
textwrap.dedent(
|
|
||||||
"""
|
|
||||||
Error: setup script specifies an absolute path:
|
|
||||||
|
|
||||||
%s
|
|
||||||
|
|
||||||
setup() arguments must *always* be /-separated paths relative to the
|
|
||||||
setup.py directory, *never* absolute paths.
|
|
||||||
"""
|
|
||||||
).lstrip()
|
|
||||||
% path
|
|
||||||
)
|
|
||||||
raise DistutilsSetupError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class _IncludePackageDataAbuse:
|
|
||||||
"""Inform users that package or module is included as 'data file'"""
|
|
||||||
|
|
||||||
MESSAGE = """\
|
|
||||||
Installing {importable!r} as data is deprecated, please list it in `packages`.
|
|
||||||
!!\n\n
|
|
||||||
############################
|
|
||||||
# Package would be ignored #
|
|
||||||
############################
|
|
||||||
Python recognizes {importable!r} as an importable package,
|
|
||||||
but it is not listed in the `packages` configuration of setuptools.
|
|
||||||
|
|
||||||
{importable!r} has been automatically added to the distribution only
|
|
||||||
because it may contain data files, but this behavior is likely to change
|
|
||||||
in future versions of setuptools (and therefore is considered deprecated).
|
|
||||||
|
|
||||||
Please make sure that {importable!r} is included as a package by using
|
|
||||||
the `packages` configuration field or the proper discovery methods
|
|
||||||
(for example by using `find_namespace_packages(...)`/`find_namespace:`
|
|
||||||
instead of `find_packages(...)`/`find:`).
|
|
||||||
|
|
||||||
You can read more about "package discovery" and "data files" on setuptools
|
|
||||||
documentation page.
|
|
||||||
\n\n!!
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._already_warned = set()
|
|
||||||
|
|
||||||
def is_module(self, file):
|
|
||||||
return file.endswith(".py") and file[:-len(".py")].isidentifier()
|
|
||||||
|
|
||||||
def importable_subpackage(self, parent, file):
|
|
||||||
pkg = Path(file).parent
|
|
||||||
parts = list(itertools.takewhile(str.isidentifier, pkg.parts))
|
|
||||||
if parts:
|
|
||||||
return ".".join([parent, *parts])
|
|
||||||
return None
|
|
||||||
|
|
||||||
def warn(self, importable):
|
|
||||||
if importable not in self._already_warned:
|
|
||||||
msg = textwrap.dedent(self.MESSAGE).format(importable=importable)
|
|
||||||
warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
|
|
||||||
self._already_warned.add(importable)
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
from distutils.util import convert_path
|
|
||||||
from distutils import log
|
|
||||||
from distutils.errors import DistutilsError, DistutilsOptionError
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
import io
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
from setuptools.command.easy_install import easy_install
|
|
||||||
from setuptools import namespaces
|
|
||||||
import setuptools
|
|
||||||
|
|
||||||
|
|
||||||
class develop(namespaces.DevelopInstaller, easy_install):
|
|
||||||
"""Set up package for development"""
|
|
||||||
|
|
||||||
description = "install package in 'development mode'"
|
|
||||||
|
|
||||||
user_options = easy_install.user_options + [
|
|
||||||
("uninstall", "u", "Uninstall this source package"),
|
|
||||||
("egg-path=", None, "Set the path to be used in the .egg-link file"),
|
|
||||||
]
|
|
||||||
|
|
||||||
boolean_options = easy_install.boolean_options + ['uninstall']
|
|
||||||
|
|
||||||
command_consumes_arguments = False # override base
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
if self.uninstall:
|
|
||||||
self.multi_version = True
|
|
||||||
self.uninstall_link()
|
|
||||||
self.uninstall_namespaces()
|
|
||||||
else:
|
|
||||||
self.install_for_development()
|
|
||||||
self.warn_deprecated_options()
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.uninstall = None
|
|
||||||
self.egg_path = None
|
|
||||||
easy_install.initialize_options(self)
|
|
||||||
self.setup_path = None
|
|
||||||
self.always_copy_from = '.' # always copy eggs installed in curdir
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
ei = self.get_finalized_command("egg_info")
|
|
||||||
if ei.broken_egg_info:
|
|
||||||
template = "Please rename %r to %r before using 'develop'"
|
|
||||||
args = ei.egg_info, ei.broken_egg_info
|
|
||||||
raise DistutilsError(template % args)
|
|
||||||
self.args = [ei.egg_name]
|
|
||||||
|
|
||||||
easy_install.finalize_options(self)
|
|
||||||
self.expand_basedirs()
|
|
||||||
self.expand_dirs()
|
|
||||||
# pick up setup-dir .egg files only: no .egg-info
|
|
||||||
self.package_index.scan(glob.glob('*.egg'))
|
|
||||||
|
|
||||||
egg_link_fn = ei.egg_name + '.egg-link'
|
|
||||||
self.egg_link = os.path.join(self.install_dir, egg_link_fn)
|
|
||||||
self.egg_base = ei.egg_base
|
|
||||||
if self.egg_path is None:
|
|
||||||
self.egg_path = os.path.abspath(ei.egg_base)
|
|
||||||
|
|
||||||
target = pkg_resources.normalize_path(self.egg_base)
|
|
||||||
egg_path = pkg_resources.normalize_path(
|
|
||||||
os.path.join(self.install_dir, self.egg_path)
|
|
||||||
)
|
|
||||||
if egg_path != target:
|
|
||||||
raise DistutilsOptionError(
|
|
||||||
"--egg-path must be a relative path from the install"
|
|
||||||
" directory to " + target
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make a distribution for the package's source
|
|
||||||
self.dist = pkg_resources.Distribution(
|
|
||||||
target,
|
|
||||||
pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)),
|
|
||||||
project_name=ei.egg_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.setup_path = self._resolve_setup_path(
|
|
||||||
self.egg_base,
|
|
||||||
self.install_dir,
|
|
||||||
self.egg_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _resolve_setup_path(egg_base, install_dir, egg_path):
|
|
||||||
"""
|
|
||||||
Generate a path from egg_base back to '.' where the
|
|
||||||
setup script resides and ensure that path points to the
|
|
||||||
setup path from $install_dir/$egg_path.
|
|
||||||
"""
|
|
||||||
path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
|
|
||||||
if path_to_setup != os.curdir:
|
|
||||||
path_to_setup = '../' * (path_to_setup.count('/') + 1)
|
|
||||||
resolved = pkg_resources.normalize_path(
|
|
||||||
os.path.join(install_dir, egg_path, path_to_setup)
|
|
||||||
)
|
|
||||||
if resolved != pkg_resources.normalize_path(os.curdir):
|
|
||||||
raise DistutilsOptionError(
|
|
||||||
"Can't get a consistent path to setup script from"
|
|
||||||
" installation directory",
|
|
||||||
resolved,
|
|
||||||
pkg_resources.normalize_path(os.curdir),
|
|
||||||
)
|
|
||||||
return path_to_setup
|
|
||||||
|
|
||||||
def install_for_development(self):
|
|
||||||
self.run_command('egg_info')
|
|
||||||
|
|
||||||
# Build extensions in-place
|
|
||||||
self.reinitialize_command('build_ext', inplace=1)
|
|
||||||
self.run_command('build_ext')
|
|
||||||
|
|
||||||
if setuptools.bootstrap_install_from:
|
|
||||||
self.easy_install(setuptools.bootstrap_install_from)
|
|
||||||
setuptools.bootstrap_install_from = None
|
|
||||||
|
|
||||||
self.install_namespaces()
|
|
||||||
|
|
||||||
# create an .egg-link in the installation dir, pointing to our egg
|
|
||||||
log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
|
|
||||||
if not self.dry_run:
|
|
||||||
with open(self.egg_link, "w") as f:
|
|
||||||
f.write(self.egg_path + "\n" + self.setup_path)
|
|
||||||
# postprocess the installed distro, fixing up .pth, installing scripts,
|
|
||||||
# and handling requirements
|
|
||||||
self.process_distribution(None, self.dist, not self.no_deps)
|
|
||||||
|
|
||||||
def uninstall_link(self):
|
|
||||||
if os.path.exists(self.egg_link):
|
|
||||||
log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
|
|
||||||
egg_link_file = open(self.egg_link)
|
|
||||||
contents = [line.rstrip() for line in egg_link_file]
|
|
||||||
egg_link_file.close()
|
|
||||||
if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
|
|
||||||
log.warn("Link points to %s: uninstall aborted", contents)
|
|
||||||
return
|
|
||||||
if not self.dry_run:
|
|
||||||
os.unlink(self.egg_link)
|
|
||||||
if not self.dry_run:
|
|
||||||
self.update_pth(self.dist) # remove any .pth link to us
|
|
||||||
if self.distribution.scripts:
|
|
||||||
# XXX should also check for entry point scripts!
|
|
||||||
log.warn("Note: you must uninstall or replace scripts manually!")
|
|
||||||
|
|
||||||
def install_egg_scripts(self, dist):
|
|
||||||
if dist is not self.dist:
|
|
||||||
# Installing a dependency, so fall back to normal behavior
|
|
||||||
return easy_install.install_egg_scripts(self, dist)
|
|
||||||
|
|
||||||
# create wrapper scripts in the script dir, pointing to dist.scripts
|
|
||||||
|
|
||||||
# new-style...
|
|
||||||
self.install_wrapper_scripts(dist)
|
|
||||||
|
|
||||||
# ...and old-style
|
|
||||||
for script_name in self.distribution.scripts or []:
|
|
||||||
script_path = os.path.abspath(convert_path(script_name))
|
|
||||||
script_name = os.path.basename(script_path)
|
|
||||||
with io.open(script_path) as strm:
|
|
||||||
script_text = strm.read()
|
|
||||||
self.install_script(dist, script_name, script_text, script_path)
|
|
||||||
|
|
||||||
def install_wrapper_scripts(self, dist):
|
|
||||||
dist = VersionlessRequirement(dist)
|
|
||||||
return easy_install.install_wrapper_scripts(self, dist)
|
|
||||||
|
|
||||||
|
|
||||||
class VersionlessRequirement:
|
|
||||||
"""
|
|
||||||
Adapt a pkg_resources.Distribution to simply return the project
|
|
||||||
name as the 'requirement' so that scripts will work across
|
|
||||||
multiple versions.
|
|
||||||
|
|
||||||
>>> from pkg_resources import Distribution
|
|
||||||
>>> dist = Distribution(project_name='foo', version='1.0')
|
|
||||||
>>> str(dist.as_requirement())
|
|
||||||
'foo==1.0'
|
|
||||||
>>> adapted_dist = VersionlessRequirement(dist)
|
|
||||||
>>> str(adapted_dist.as_requirement())
|
|
||||||
'foo'
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, dist):
|
|
||||||
self.__dist = dist
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self.__dist, name)
|
|
||||||
|
|
||||||
def as_requirement(self):
|
|
||||||
return self.project_name
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
"""
|
|
||||||
Create a dist_info directory
|
|
||||||
As defined in the wheel specification
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from inspect import cleandoc
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from distutils.core import Command
|
|
||||||
from distutils import log
|
|
||||||
from setuptools.extern import packaging
|
|
||||||
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
|
|
||||||
|
|
||||||
|
|
||||||
class dist_info(Command):
|
|
||||||
|
|
||||||
description = 'create a .dist-info directory'
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
('egg-base=', 'e', "directory containing .egg-info directories"
|
|
||||||
" (default: top of the source tree)"
|
|
||||||
" DEPRECATED: use --output-dir."),
|
|
||||||
('output-dir=', 'o', "directory inside of which the .dist-info will be"
|
|
||||||
"created (default: top of the source tree)"),
|
|
||||||
('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),
|
|
||||||
('tag-build=', 'b', "Specify explicit tag to add to version number"),
|
|
||||||
('no-date', 'D', "Don't include date stamp [default]"),
|
|
||||||
('keep-egg-info', None, "*TRANSITIONAL* will be removed in the future"),
|
|
||||||
]
|
|
||||||
|
|
||||||
boolean_options = ['tag-date', 'keep-egg-info']
|
|
||||||
negative_opt = {'no-date': 'tag-date'}
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.egg_base = None
|
|
||||||
self.output_dir = None
|
|
||||||
self.name = None
|
|
||||||
self.dist_info_dir = None
|
|
||||||
self.tag_date = None
|
|
||||||
self.tag_build = None
|
|
||||||
self.keep_egg_info = False
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
if self.egg_base:
|
|
||||||
msg = "--egg-base is deprecated for dist_info command. Use --output-dir."
|
|
||||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
|
||||||
self.output_dir = self.egg_base or self.output_dir
|
|
||||||
|
|
||||||
dist = self.distribution
|
|
||||||
project_dir = dist.src_root or os.curdir
|
|
||||||
self.output_dir = Path(self.output_dir or project_dir)
|
|
||||||
|
|
||||||
egg_info = self.reinitialize_command("egg_info")
|
|
||||||
egg_info.egg_base = str(self.output_dir)
|
|
||||||
|
|
||||||
if self.tag_date:
|
|
||||||
egg_info.tag_date = self.tag_date
|
|
||||||
else:
|
|
||||||
self.tag_date = egg_info.tag_date
|
|
||||||
|
|
||||||
if self.tag_build:
|
|
||||||
egg_info.tag_build = self.tag_build
|
|
||||||
else:
|
|
||||||
self.tag_build = egg_info.tag_build
|
|
||||||
|
|
||||||
egg_info.finalize_options()
|
|
||||||
self.egg_info = egg_info
|
|
||||||
|
|
||||||
name = _safe(dist.get_name())
|
|
||||||
version = _version(dist.get_version())
|
|
||||||
self.name = f"{name}-{version}"
|
|
||||||
self.dist_info_dir = os.path.join(self.output_dir, f"{self.name}.dist-info")
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def _maybe_bkp_dir(self, dir_path: str, requires_bkp: bool):
|
|
||||||
if requires_bkp:
|
|
||||||
bkp_name = f"{dir_path}.__bkp__"
|
|
||||||
_rm(bkp_name, ignore_errors=True)
|
|
||||||
_copy(dir_path, bkp_name, dirs_exist_ok=True, symlinks=True)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
_rm(dir_path, ignore_errors=True)
|
|
||||||
shutil.move(bkp_name, dir_path)
|
|
||||||
else:
|
|
||||||
yield
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
self.egg_info.run()
|
|
||||||
egg_info_dir = self.egg_info.egg_info
|
|
||||||
assert os.path.isdir(egg_info_dir), ".egg-info dir should have been created"
|
|
||||||
|
|
||||||
log.info("creating '{}'".format(os.path.abspath(self.dist_info_dir)))
|
|
||||||
bdist_wheel = self.get_finalized_command('bdist_wheel')
|
|
||||||
|
|
||||||
# TODO: if bdist_wheel if merged into setuptools, just add "keep_egg_info" there
|
|
||||||
with self._maybe_bkp_dir(egg_info_dir, self.keep_egg_info):
|
|
||||||
bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def _safe(component: str) -> str:
|
|
||||||
"""Escape a component used to form a wheel name according to PEP 491"""
|
|
||||||
return re.sub(r"[^\w\d.]+", "_", component)
|
|
||||||
|
|
||||||
|
|
||||||
def _version(version: str) -> str:
|
|
||||||
"""Convert an arbitrary string to a version string."""
|
|
||||||
v = version.replace(' ', '.')
|
|
||||||
try:
|
|
||||||
return str(packaging.version.Version(v)).replace("-", "_")
|
|
||||||
except packaging.version.InvalidVersion:
|
|
||||||
msg = f"""Invalid version: {version!r}.
|
|
||||||
!!\n\n
|
|
||||||
###################
|
|
||||||
# Invalid version #
|
|
||||||
###################
|
|
||||||
{version!r} is not valid according to PEP 440.\n
|
|
||||||
Please make sure specify a valid version for your package.
|
|
||||||
Also note that future releases of setuptools may halt the build process
|
|
||||||
if an invalid version is given.
|
|
||||||
\n\n!!
|
|
||||||
"""
|
|
||||||
warnings.warn(cleandoc(msg))
|
|
||||||
return _safe(v).strip("_")
|
|
||||||
|
|
||||||
|
|
||||||
def _rm(dir_name, **opts):
|
|
||||||
if os.path.isdir(dir_name):
|
|
||||||
shutil.rmtree(dir_name, **opts)
|
|
||||||
|
|
||||||
|
|
||||||
def _copy(src, dst, **opts):
|
|
||||||
if sys.version_info < (3, 8):
|
|
||||||
opts.pop("dirs_exist_ok", None)
|
|
||||||
shutil.copytree(src, dst, **opts)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,844 +0,0 @@
|
|||||||
"""
|
|
||||||
Create a wheel that, when installed, will make the source package 'editable'
|
|
||||||
(add it to the interpreter's path, including metadata) per PEP 660. Replaces
|
|
||||||
'setup.py develop'.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
One of the mechanisms briefly mentioned in PEP 660 to implement editable installs is
|
|
||||||
to create a separated directory inside ``build`` and use a .pth file to point to that
|
|
||||||
directory. In the context of this file such directory is referred as
|
|
||||||
*auxiliary build directory* or ``auxiliary_dir``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
import warnings
|
|
||||||
from contextlib import suppress
|
|
||||||
from enum import Enum
|
|
||||||
from inspect import cleandoc
|
|
||||||
from itertools import chain
|
|
||||||
from pathlib import Path
|
|
||||||
from tempfile import TemporaryDirectory
|
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Mapping,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
from setuptools import Command, SetuptoolsDeprecationWarning, errors, namespaces
|
|
||||||
from setuptools.command.build_py import build_py as build_py_cls
|
|
||||||
from setuptools.discovery import find_package_path
|
|
||||||
from setuptools.dist import Distribution
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from wheel.wheelfile import WheelFile # noqa
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
from typing import Protocol
|
|
||||||
elif TYPE_CHECKING:
|
|
||||||
from typing_extensions import Protocol
|
|
||||||
else:
|
|
||||||
from abc import ABC as Protocol
|
|
||||||
|
|
||||||
_Path = Union[str, Path]
|
|
||||||
_P = TypeVar("_P", bound=_Path)
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class _EditableMode(Enum):
|
|
||||||
"""
|
|
||||||
Possible editable installation modes:
|
|
||||||
`lenient` (new files automatically added to the package - DEFAULT);
|
|
||||||
`strict` (requires a new installation when files are added/removed); or
|
|
||||||
`compat` (attempts to emulate `python setup.py develop` - DEPRECATED).
|
|
||||||
"""
|
|
||||||
|
|
||||||
STRICT = "strict"
|
|
||||||
LENIENT = "lenient"
|
|
||||||
COMPAT = "compat" # TODO: Remove `compat` after Dec/2022.
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert(cls, mode: Optional[str]) -> "_EditableMode":
|
|
||||||
if not mode:
|
|
||||||
return _EditableMode.LENIENT # default
|
|
||||||
|
|
||||||
_mode = mode.upper()
|
|
||||||
if _mode not in _EditableMode.__members__:
|
|
||||||
raise errors.OptionError(f"Invalid editable mode: {mode!r}. Try: 'strict'.")
|
|
||||||
|
|
||||||
if _mode == "COMPAT":
|
|
||||||
msg = """
|
|
||||||
The 'compat' editable mode is transitional and will be removed
|
|
||||||
in future versions of `setuptools`.
|
|
||||||
Please adapt your code accordingly to use either the 'strict' or the
|
|
||||||
'lenient' modes.
|
|
||||||
|
|
||||||
For more information, please check:
|
|
||||||
https://setuptools.pypa.io/en/latest/userguide/development_mode.html
|
|
||||||
"""
|
|
||||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
|
||||||
|
|
||||||
return _EditableMode[_mode]
|
|
||||||
|
|
||||||
|
|
||||||
_STRICT_WARNING = """
|
|
||||||
New or renamed files may not be automatically picked up without a new installation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_LENIENT_WARNING = """
|
|
||||||
Options like `package-data`, `include/exclude-package-data` or
|
|
||||||
`packages.find.exclude/include` may have no effect.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class editable_wheel(Command):
|
|
||||||
"""Build 'editable' wheel for development.
|
|
||||||
(This command is reserved for internal use of setuptools).
|
|
||||||
"""
|
|
||||||
|
|
||||||
description = "create a PEP 660 'editable' wheel"
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
("dist-dir=", "d", "directory to put final built distributions in"),
|
|
||||||
("dist-info-dir=", "I", "path to a pre-build .dist-info directory"),
|
|
||||||
("mode=", None, cleandoc(_EditableMode.__doc__ or "")),
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.dist_dir = None
|
|
||||||
self.dist_info_dir = None
|
|
||||||
self.project_dir = None
|
|
||||||
self.mode = None
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
dist = self.distribution
|
|
||||||
self.project_dir = dist.src_root or os.curdir
|
|
||||||
self.package_dir = dist.package_dir or {}
|
|
||||||
self.dist_dir = Path(self.dist_dir or os.path.join(self.project_dir, "dist"))
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
self.dist_dir.mkdir(exist_ok=True)
|
|
||||||
self._ensure_dist_info()
|
|
||||||
|
|
||||||
# Add missing dist_info files
|
|
||||||
self.reinitialize_command("bdist_wheel")
|
|
||||||
bdist_wheel = self.get_finalized_command("bdist_wheel")
|
|
||||||
bdist_wheel.write_wheelfile(self.dist_info_dir)
|
|
||||||
|
|
||||||
self._create_wheel_file(bdist_wheel)
|
|
||||||
except Exception as ex:
|
|
||||||
traceback.print_exc()
|
|
||||||
msg = """
|
|
||||||
Support for editable installs via PEP 660 was recently introduced
|
|
||||||
in `setuptools`. If you are seeing this error, please report to:
|
|
||||||
|
|
||||||
https://github.com/pypa/setuptools/issues
|
|
||||||
|
|
||||||
Meanwhile you can try the legacy behavior by setting an
|
|
||||||
environment variable and trying to install again:
|
|
||||||
|
|
||||||
SETUPTOOLS_ENABLE_FEATURES="legacy-editable"
|
|
||||||
"""
|
|
||||||
raise errors.InternalError(cleandoc(msg)) from ex
|
|
||||||
|
|
||||||
def _ensure_dist_info(self):
|
|
||||||
if self.dist_info_dir is None:
|
|
||||||
dist_info = self.reinitialize_command("dist_info")
|
|
||||||
dist_info.output_dir = self.dist_dir
|
|
||||||
dist_info.ensure_finalized()
|
|
||||||
dist_info.run()
|
|
||||||
self.dist_info_dir = dist_info.dist_info_dir
|
|
||||||
else:
|
|
||||||
assert str(self.dist_info_dir).endswith(".dist-info")
|
|
||||||
assert Path(self.dist_info_dir, "METADATA").exists()
|
|
||||||
|
|
||||||
def _install_namespaces(self, installation_dir, pth_prefix):
|
|
||||||
# XXX: Only required to support the deprecated namespace practice
|
|
||||||
dist = self.distribution
|
|
||||||
if not dist.namespace_packages:
|
|
||||||
return
|
|
||||||
|
|
||||||
src_root = Path(self.project_dir, self.package_dir.get("", ".")).resolve()
|
|
||||||
installer = _NamespaceInstaller(dist, installation_dir, pth_prefix, src_root)
|
|
||||||
installer.install_namespaces()
|
|
||||||
|
|
||||||
def _find_egg_info_dir(self) -> Optional[str]:
|
|
||||||
parent_dir = Path(self.dist_info_dir).parent if self.dist_info_dir else Path()
|
|
||||||
candidates = map(str, parent_dir.glob("*.egg-info"))
|
|
||||||
return next(candidates, None)
|
|
||||||
|
|
||||||
def _configure_build(
|
|
||||||
self, name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
|
|
||||||
):
|
|
||||||
"""Configure commands to behave in the following ways:
|
|
||||||
|
|
||||||
- Build commands can write to ``build_lib`` if they really want to...
|
|
||||||
(but this folder is expected to be ignored and modules are expected to live
|
|
||||||
in the project directory...)
|
|
||||||
- Binary extensions should be built in-place (editable_mode = True)
|
|
||||||
- Data/header/script files are not part of the "editable" specification
|
|
||||||
so they are written directly to the unpacked_wheel directory.
|
|
||||||
"""
|
|
||||||
# Non-editable files (data, headers, scripts) are written directly to the
|
|
||||||
# unpacked_wheel
|
|
||||||
|
|
||||||
dist = self.distribution
|
|
||||||
wheel = str(unpacked_wheel)
|
|
||||||
build_lib = str(build_lib)
|
|
||||||
data = str(Path(unpacked_wheel, f"{name}.data", "data"))
|
|
||||||
headers = str(Path(unpacked_wheel, f"{name}.data", "headers"))
|
|
||||||
scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts"))
|
|
||||||
|
|
||||||
# egg-info may be generated again to create a manifest (used for package data)
|
|
||||||
egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True)
|
|
||||||
egg_info.egg_base = str(tmp_dir)
|
|
||||||
egg_info.ignore_egg_info_in_manifest = True
|
|
||||||
|
|
||||||
build = dist.reinitialize_command("build", reinit_subcommands=True)
|
|
||||||
install = dist.reinitialize_command("install", reinit_subcommands=True)
|
|
||||||
|
|
||||||
build.build_platlib = build.build_purelib = build.build_lib = build_lib
|
|
||||||
install.install_purelib = install.install_platlib = install.install_lib = wheel
|
|
||||||
install.install_scripts = build.build_scripts = scripts
|
|
||||||
install.install_headers = headers
|
|
||||||
install.install_data = data
|
|
||||||
|
|
||||||
install_scripts = dist.get_command_obj("install_scripts")
|
|
||||||
install_scripts.no_ep = True
|
|
||||||
|
|
||||||
build.build_temp = str(tmp_dir)
|
|
||||||
|
|
||||||
build_py = dist.get_command_obj("build_py")
|
|
||||||
build_py.compile = False
|
|
||||||
build_py.existing_egg_info_dir = self._find_egg_info_dir()
|
|
||||||
|
|
||||||
self._set_editable_mode()
|
|
||||||
|
|
||||||
build.ensure_finalized()
|
|
||||||
install.ensure_finalized()
|
|
||||||
|
|
||||||
def _set_editable_mode(self):
|
|
||||||
"""Set the ``editable_mode`` flag in the build sub-commands"""
|
|
||||||
dist = self.distribution
|
|
||||||
build = dist.get_command_obj("build")
|
|
||||||
for cmd_name in build.get_sub_commands():
|
|
||||||
cmd = dist.get_command_obj(cmd_name)
|
|
||||||
if hasattr(cmd, "editable_mode"):
|
|
||||||
cmd.editable_mode = True
|
|
||||||
elif hasattr(cmd, "inplace"):
|
|
||||||
cmd.inplace = True # backward compatibility with distutils
|
|
||||||
|
|
||||||
def _collect_build_outputs(self) -> Tuple[List[str], Dict[str, str]]:
|
|
||||||
files: List[str] = []
|
|
||||||
mapping: Dict[str, str] = {}
|
|
||||||
build = self.get_finalized_command("build")
|
|
||||||
|
|
||||||
for cmd_name in build.get_sub_commands():
|
|
||||||
cmd = self.get_finalized_command(cmd_name)
|
|
||||||
if hasattr(cmd, "get_outputs"):
|
|
||||||
files.extend(cmd.get_outputs() or [])
|
|
||||||
if hasattr(cmd, "get_output_mapping"):
|
|
||||||
mapping.update(cmd.get_output_mapping() or {})
|
|
||||||
|
|
||||||
return files, mapping
|
|
||||||
|
|
||||||
def _run_build_commands(
|
|
||||||
self, dist_name: str, unpacked_wheel: _Path, build_lib: _Path, tmp_dir: _Path
|
|
||||||
) -> Tuple[List[str], Dict[str, str]]:
|
|
||||||
self._configure_build(dist_name, unpacked_wheel, build_lib, tmp_dir)
|
|
||||||
self._run_build_subcommands()
|
|
||||||
files, mapping = self._collect_build_outputs()
|
|
||||||
self._run_install("headers")
|
|
||||||
self._run_install("scripts")
|
|
||||||
self._run_install("data")
|
|
||||||
return files, mapping
|
|
||||||
|
|
||||||
def _run_build_subcommands(self):
|
|
||||||
"""
|
|
||||||
Issue #3501 indicates that some plugins/customizations might rely on:
|
|
||||||
|
|
||||||
1. ``build_py`` not running
|
|
||||||
2. ``build_py`` always copying files to ``build_lib``
|
|
||||||
|
|
||||||
However both these assumptions may be false in editable_wheel.
|
|
||||||
This method implements a temporary workaround to support the ecosystem
|
|
||||||
while the implementations catch up.
|
|
||||||
"""
|
|
||||||
# TODO: Once plugins/customisations had the chance to catch up, replace
|
|
||||||
# `self._run_build_subcommands()` with `self.run_command("build")`.
|
|
||||||
# Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023.
|
|
||||||
build: Command = self.get_finalized_command("build")
|
|
||||||
for name in build.get_sub_commands():
|
|
||||||
cmd = self.get_finalized_command(name)
|
|
||||||
if name == "build_py" and type(cmd) != build_py_cls:
|
|
||||||
self._safely_run(name)
|
|
||||||
else:
|
|
||||||
self.run_command(name)
|
|
||||||
|
|
||||||
def _safely_run(self, cmd_name: str):
|
|
||||||
try:
|
|
||||||
return self.run_command(cmd_name)
|
|
||||||
except Exception:
|
|
||||||
msg = f"""{traceback.format_exc()}\n
|
|
||||||
If you are seeing this warning it is very likely that a setuptools
|
|
||||||
plugin or customization overrides the `{cmd_name}` command, without
|
|
||||||
taking into consideration how editable installs run build steps
|
|
||||||
starting from v64.0.0.
|
|
||||||
|
|
||||||
Plugin authors and developers relying on custom build steps are encouraged
|
|
||||||
to update their `{cmd_name}` implementation considering the information in
|
|
||||||
https://setuptools.pypa.io/en/latest/userguide/extension.html
|
|
||||||
about editable installs.
|
|
||||||
|
|
||||||
For the time being `setuptools` will silence this error and ignore
|
|
||||||
the faulty command, but this behaviour will change in future versions.\n
|
|
||||||
"""
|
|
||||||
warnings.warn(msg, SetuptoolsDeprecationWarning, stacklevel=2)
|
|
||||||
|
|
||||||
def _create_wheel_file(self, bdist_wheel):
|
|
||||||
from wheel.wheelfile import WheelFile
|
|
||||||
|
|
||||||
dist_info = self.get_finalized_command("dist_info")
|
|
||||||
dist_name = dist_info.name
|
|
||||||
tag = "-".join(bdist_wheel.get_tag())
|
|
||||||
build_tag = "0.editable" # According to PEP 427 needs to start with digit
|
|
||||||
archive_name = f"{dist_name}-{build_tag}-{tag}.whl"
|
|
||||||
wheel_path = Path(self.dist_dir, archive_name)
|
|
||||||
if wheel_path.exists():
|
|
||||||
wheel_path.unlink()
|
|
||||||
|
|
||||||
unpacked_wheel = TemporaryDirectory(suffix=archive_name)
|
|
||||||
build_lib = TemporaryDirectory(suffix=".build-lib")
|
|
||||||
build_tmp = TemporaryDirectory(suffix=".build-temp")
|
|
||||||
|
|
||||||
with unpacked_wheel as unpacked, build_lib as lib, build_tmp as tmp:
|
|
||||||
unpacked_dist_info = Path(unpacked, Path(self.dist_info_dir).name)
|
|
||||||
shutil.copytree(self.dist_info_dir, unpacked_dist_info)
|
|
||||||
self._install_namespaces(unpacked, dist_info.name)
|
|
||||||
files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
|
|
||||||
strategy = self._select_strategy(dist_name, tag, lib)
|
|
||||||
with strategy, WheelFile(wheel_path, "w") as wheel_obj:
|
|
||||||
strategy(wheel_obj, files, mapping)
|
|
||||||
wheel_obj.write_files(unpacked)
|
|
||||||
|
|
||||||
return wheel_path
|
|
||||||
|
|
||||||
def _run_install(self, category: str):
|
|
||||||
has_category = getattr(self.distribution, f"has_{category}", None)
|
|
||||||
if has_category and has_category():
|
|
||||||
_logger.info(f"Installing {category} as non editable")
|
|
||||||
self.run_command(f"install_{category}")
|
|
||||||
|
|
||||||
def _select_strategy(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
tag: str,
|
|
||||||
build_lib: _Path,
|
|
||||||
) -> "EditableStrategy":
|
|
||||||
"""Decides which strategy to use to implement an editable installation."""
|
|
||||||
build_name = f"__editable__.{name}-{tag}"
|
|
||||||
project_dir = Path(self.project_dir)
|
|
||||||
mode = _EditableMode.convert(self.mode)
|
|
||||||
|
|
||||||
if mode is _EditableMode.STRICT:
|
|
||||||
auxiliary_dir = _empty_dir(Path(self.project_dir, "build", build_name))
|
|
||||||
return _LinkTree(self.distribution, name, auxiliary_dir, build_lib)
|
|
||||||
|
|
||||||
packages = _find_packages(self.distribution)
|
|
||||||
has_simple_layout = _simple_layout(packages, self.package_dir, project_dir)
|
|
||||||
is_compat_mode = mode is _EditableMode.COMPAT
|
|
||||||
if set(self.package_dir) == {""} and has_simple_layout or is_compat_mode:
|
|
||||||
# src-layout(ish) is relatively safe for a simple pth file
|
|
||||||
src_dir = self.package_dir.get("", ".")
|
|
||||||
return _StaticPth(self.distribution, name, [Path(project_dir, src_dir)])
|
|
||||||
|
|
||||||
# Use a MetaPathFinder to avoid adding accidental top-level packages/modules
|
|
||||||
return _TopLevelFinder(self.distribution, name)
|
|
||||||
|
|
||||||
|
|
||||||
class EditableStrategy(Protocol):
|
|
||||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
|
||||||
...
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class _StaticPth:
|
|
||||||
def __init__(self, dist: Distribution, name: str, path_entries: List[Path]):
|
|
||||||
self.dist = dist
|
|
||||||
self.name = name
|
|
||||||
self.path_entries = path_entries
|
|
||||||
|
|
||||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
|
||||||
entries = "\n".join((str(p.resolve()) for p in self.path_entries))
|
|
||||||
contents = bytes(f"{entries}\n", "utf-8")
|
|
||||||
wheel.writestr(f"__editable__.{self.name}.pth", contents)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
msg = f"""
|
|
||||||
Editable install will be performed using .pth file to extend `sys.path` with:
|
|
||||||
{list(map(os.fspath, self.path_entries))!r}
|
|
||||||
"""
|
|
||||||
_logger.warning(msg + _LENIENT_WARNING)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class _LinkTree(_StaticPth):
|
|
||||||
"""
|
|
||||||
Creates a ``.pth`` file that points to a link tree in the ``auxiliary_dir``.
|
|
||||||
|
|
||||||
This strategy will only link files (not dirs), so it can be implemented in
|
|
||||||
any OS, even if that means using hardlinks instead of symlinks.
|
|
||||||
|
|
||||||
By collocating ``auxiliary_dir`` and the original source code, limitations
|
|
||||||
with hardlinks should be avoided.
|
|
||||||
"""
|
|
||||||
def __init__(
|
|
||||||
self, dist: Distribution,
|
|
||||||
name: str,
|
|
||||||
auxiliary_dir: _Path,
|
|
||||||
build_lib: _Path,
|
|
||||||
):
|
|
||||||
self.auxiliary_dir = Path(auxiliary_dir)
|
|
||||||
self.build_lib = Path(build_lib).resolve()
|
|
||||||
self._file = dist.get_command_obj("build_py").copy_file
|
|
||||||
super().__init__(dist, name, [self.auxiliary_dir])
|
|
||||||
|
|
||||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
|
||||||
self._create_links(files, mapping)
|
|
||||||
super().__call__(wheel, files, mapping)
|
|
||||||
|
|
||||||
def _normalize_output(self, file: str) -> Optional[str]:
|
|
||||||
# Files relative to build_lib will be normalized to None
|
|
||||||
with suppress(ValueError):
|
|
||||||
path = Path(file).resolve().relative_to(self.build_lib)
|
|
||||||
return str(path).replace(os.sep, '/')
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _create_file(self, relative_output: str, src_file: str, link=None):
|
|
||||||
dest = self.auxiliary_dir / relative_output
|
|
||||||
if not dest.parent.is_dir():
|
|
||||||
dest.parent.mkdir(parents=True)
|
|
||||||
self._file(src_file, dest, link=link)
|
|
||||||
|
|
||||||
def _create_links(self, outputs, output_mapping):
|
|
||||||
self.auxiliary_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
link_type = "sym" if _can_symlink_files(self.auxiliary_dir) else "hard"
|
|
||||||
mappings = {
|
|
||||||
self._normalize_output(k): v
|
|
||||||
for k, v in output_mapping.items()
|
|
||||||
}
|
|
||||||
mappings.pop(None, None) # remove files that are not relative to build_lib
|
|
||||||
|
|
||||||
for output in outputs:
|
|
||||||
relative = self._normalize_output(output)
|
|
||||||
if relative and relative not in mappings:
|
|
||||||
self._create_file(relative, output)
|
|
||||||
|
|
||||||
for relative, src in mappings.items():
|
|
||||||
self._create_file(relative, src, link=link_type)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
msg = "Strict editable install will be performed using a link tree.\n"
|
|
||||||
_logger.warning(msg + _STRICT_WARNING)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
||||||
msg = f"""\n
|
|
||||||
Strict editable installation performed using the auxiliary directory:
|
|
||||||
{self.auxiliary_dir}
|
|
||||||
|
|
||||||
Please be careful to not remove this directory, otherwise you might not be able
|
|
||||||
to import/use your package.
|
|
||||||
"""
|
|
||||||
warnings.warn(msg, InformationOnly)
|
|
||||||
|
|
||||||
|
|
||||||
class _TopLevelFinder:
|
|
||||||
def __init__(self, dist: Distribution, name: str):
|
|
||||||
self.dist = dist
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]):
|
|
||||||
src_root = self.dist.src_root or os.curdir
|
|
||||||
top_level = chain(_find_packages(self.dist), _find_top_level_modules(self.dist))
|
|
||||||
package_dir = self.dist.package_dir or {}
|
|
||||||
roots = _find_package_roots(top_level, package_dir, src_root)
|
|
||||||
|
|
||||||
namespaces_: Dict[str, List[str]] = dict(chain(
|
|
||||||
_find_namespaces(self.dist.packages or [], roots),
|
|
||||||
((ns, []) for ns in _find_virtual_namespaces(roots)),
|
|
||||||
))
|
|
||||||
|
|
||||||
name = f"__editable__.{self.name}.finder"
|
|
||||||
finder = _make_identifier(name)
|
|
||||||
content = bytes(_finder_template(name, roots, namespaces_), "utf-8")
|
|
||||||
wheel.writestr(f"{finder}.py", content)
|
|
||||||
|
|
||||||
content = bytes(f"import {finder}; {finder}.install()", "utf-8")
|
|
||||||
wheel.writestr(f"__editable__.{self.name}.pth", content)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
msg = "Editable install will be performed using a meta path finder.\n"
|
|
||||||
_logger.warning(msg + _LENIENT_WARNING)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
||||||
msg = """\n
|
|
||||||
Please be careful with folders in your working directory with the same
|
|
||||||
name as your package as they may take precedence during imports.
|
|
||||||
"""
|
|
||||||
warnings.warn(msg, InformationOnly)
|
|
||||||
|
|
||||||
|
|
||||||
def _can_symlink_files(base_dir: Path) -> bool:
|
|
||||||
with TemporaryDirectory(dir=str(base_dir.resolve())) as tmp:
|
|
||||||
path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt")
|
|
||||||
path1.write_text("file1", encoding="utf-8")
|
|
||||||
with suppress(AttributeError, NotImplementedError, OSError):
|
|
||||||
os.symlink(path1, path2)
|
|
||||||
if path2.is_symlink() and path2.read_text(encoding="utf-8") == "file1":
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.link(path1, path2) # Ensure hard links can be created
|
|
||||||
except Exception as ex:
|
|
||||||
msg = (
|
|
||||||
"File system does not seem to support either symlinks or hard links. "
|
|
||||||
"Strict editable installs require one of them to be supported."
|
|
||||||
)
|
|
||||||
raise LinksNotSupported(msg) from ex
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _simple_layout(
|
|
||||||
packages: Iterable[str], package_dir: Dict[str, str], project_dir: Path
|
|
||||||
) -> bool:
|
|
||||||
"""Return ``True`` if:
|
|
||||||
- all packages are contained by the same parent directory, **and**
|
|
||||||
- all packages become importable if the parent directory is added to ``sys.path``.
|
|
||||||
|
|
||||||
>>> _simple_layout(['a'], {"": "src"}, "/tmp/myproj")
|
|
||||||
True
|
|
||||||
>>> _simple_layout(['a', 'a.b'], {"": "src"}, "/tmp/myproj")
|
|
||||||
True
|
|
||||||
>>> _simple_layout(['a', 'a.b'], {}, "/tmp/myproj")
|
|
||||||
True
|
|
||||||
>>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"": "src"}, "/tmp/myproj")
|
|
||||||
True
|
|
||||||
>>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "a", "b": "b"}, ".")
|
|
||||||
True
|
|
||||||
>>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a", "b": "_b"}, ".")
|
|
||||||
False
|
|
||||||
>>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a": "_a"}, "/tmp/myproj")
|
|
||||||
False
|
|
||||||
>>> _simple_layout(['a', 'a.a1', 'a.a1.a2', 'b'], {"a.a1.a2": "_a2"}, ".")
|
|
||||||
False
|
|
||||||
>>> _simple_layout(['a', 'a.b'], {"": "src", "a.b": "_ab"}, "/tmp/myproj")
|
|
||||||
False
|
|
||||||
>>> # Special cases, no packages yet:
|
|
||||||
>>> _simple_layout([], {"": "src"}, "/tmp/myproj")
|
|
||||||
True
|
|
||||||
>>> _simple_layout([], {"a": "_a", "": "src"}, "/tmp/myproj")
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
layout = {
|
|
||||||
pkg: find_package_path(pkg, package_dir, project_dir)
|
|
||||||
for pkg in packages
|
|
||||||
}
|
|
||||||
if not layout:
|
|
||||||
return set(package_dir) in ({}, {""})
|
|
||||||
parent = os.path.commonpath([_parent_path(k, v) for k, v in layout.items()])
|
|
||||||
return all(
|
|
||||||
_normalize_path(Path(parent, *key.split('.'))) == _normalize_path(value)
|
|
||||||
for key, value in layout.items()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _parent_path(pkg, pkg_path):
|
|
||||||
"""Infer the parent path containing a package, that if added to ``sys.path`` would
|
|
||||||
allow importing that package.
|
|
||||||
When ``pkg`` is directly mapped into a directory with a different name, return its
|
|
||||||
own path.
|
|
||||||
>>> _parent_path("a", "src/a")
|
|
||||||
'src'
|
|
||||||
>>> _parent_path("b", "src/c")
|
|
||||||
'src/c'
|
|
||||||
"""
|
|
||||||
parent = pkg_path[:-len(pkg)] if pkg_path.endswith(pkg) else pkg_path
|
|
||||||
return parent.rstrip("/" + os.sep)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_packages(dist: Distribution) -> Iterator[str]:
|
|
||||||
yield from iter(dist.packages or [])
|
|
||||||
|
|
||||||
py_modules = dist.py_modules or []
|
|
||||||
nested_modules = [mod for mod in py_modules if "." in mod]
|
|
||||||
if dist.ext_package:
|
|
||||||
yield dist.ext_package
|
|
||||||
else:
|
|
||||||
ext_modules = dist.ext_modules or []
|
|
||||||
nested_modules += [x.name for x in ext_modules if "." in x.name]
|
|
||||||
|
|
||||||
for module in nested_modules:
|
|
||||||
package, _, _ = module.rpartition(".")
|
|
||||||
yield package
|
|
||||||
|
|
||||||
|
|
||||||
def _find_top_level_modules(dist: Distribution) -> Iterator[str]:
|
|
||||||
py_modules = dist.py_modules or []
|
|
||||||
yield from (mod for mod in py_modules if "." not in mod)
|
|
||||||
|
|
||||||
if not dist.ext_package:
|
|
||||||
ext_modules = dist.ext_modules or []
|
|
||||||
yield from (x.name for x in ext_modules if "." not in x.name)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_package_roots(
|
|
||||||
packages: Iterable[str],
|
|
||||||
package_dir: Mapping[str, str],
|
|
||||||
src_root: _Path,
|
|
||||||
) -> Dict[str, str]:
|
|
||||||
pkg_roots: Dict[str, str] = {
|
|
||||||
pkg: _absolute_root(find_package_path(pkg, package_dir, src_root))
|
|
||||||
for pkg in sorted(packages)
|
|
||||||
}
|
|
||||||
|
|
||||||
return _remove_nested(pkg_roots)
|
|
||||||
|
|
||||||
|
|
||||||
def _absolute_root(path: _Path) -> str:
|
|
||||||
"""Works for packages and top-level modules"""
|
|
||||||
path_ = Path(path)
|
|
||||||
parent = path_.parent
|
|
||||||
|
|
||||||
if path_.exists():
|
|
||||||
return str(path_.resolve())
|
|
||||||
else:
|
|
||||||
return str(parent.resolve() / path_.name)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_virtual_namespaces(pkg_roots: Dict[str, str]) -> Iterator[str]:
|
|
||||||
"""By carefully designing ``package_dir``, it is possible to implement the logical
|
|
||||||
structure of PEP 420 in a package without the corresponding directories.
|
|
||||||
|
|
||||||
Moreover a parent package can be purposefully/accidentally skipped in the discovery
|
|
||||||
phase (e.g. ``find_packages(include=["mypkg.*"])``, when ``mypkg.foo`` is included
|
|
||||||
by ``mypkg`` itself is not).
|
|
||||||
We consider this case to also be a virtual namespace (ignoring the original
|
|
||||||
directory) to emulate a non-editable installation.
|
|
||||||
|
|
||||||
This function will try to find these kinds of namespaces.
|
|
||||||
"""
|
|
||||||
for pkg in pkg_roots:
|
|
||||||
if "." not in pkg:
|
|
||||||
continue
|
|
||||||
parts = pkg.split(".")
|
|
||||||
for i in range(len(parts) - 1, 0, -1):
|
|
||||||
partial_name = ".".join(parts[:i])
|
|
||||||
path = Path(find_package_path(partial_name, pkg_roots, ""))
|
|
||||||
if not path.exists() or partial_name not in pkg_roots:
|
|
||||||
# partial_name not in pkg_roots ==> purposefully/accidentally skipped
|
|
||||||
yield partial_name
|
|
||||||
|
|
||||||
|
|
||||||
def _find_namespaces(
|
|
||||||
packages: List[str], pkg_roots: Dict[str, str]
|
|
||||||
) -> Iterator[Tuple[str, List[str]]]:
|
|
||||||
for pkg in packages:
|
|
||||||
path = find_package_path(pkg, pkg_roots, "")
|
|
||||||
if Path(path).exists() and not Path(path, "__init__.py").exists():
|
|
||||||
yield (pkg, [path])
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_nested(pkg_roots: Dict[str, str]) -> Dict[str, str]:
|
|
||||||
output = dict(pkg_roots.copy())
|
|
||||||
|
|
||||||
for pkg, path in reversed(list(pkg_roots.items())):
|
|
||||||
if any(
|
|
||||||
pkg != other and _is_nested(pkg, path, other, other_path)
|
|
||||||
for other, other_path in pkg_roots.items()
|
|
||||||
):
|
|
||||||
output.pop(pkg)
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
def _is_nested(pkg: str, pkg_path: str, parent: str, parent_path: str) -> bool:
|
|
||||||
"""
|
|
||||||
Return ``True`` if ``pkg`` is nested inside ``parent`` both logically and in the
|
|
||||||
file system.
|
|
||||||
>>> _is_nested("a.b", "path/a/b", "a", "path/a")
|
|
||||||
True
|
|
||||||
>>> _is_nested("a.b", "path/a/b", "a", "otherpath/a")
|
|
||||||
False
|
|
||||||
>>> _is_nested("a.b", "path/a/b", "c", "path/c")
|
|
||||||
False
|
|
||||||
>>> _is_nested("a.a", "path/a/a", "a", "path/a")
|
|
||||||
True
|
|
||||||
>>> _is_nested("b.a", "path/b/a", "a", "path/a")
|
|
||||||
False
|
|
||||||
"""
|
|
||||||
norm_pkg_path = _normalize_path(pkg_path)
|
|
||||||
rest = pkg.replace(parent, "", 1).strip(".").split(".")
|
|
||||||
return (
|
|
||||||
pkg.startswith(parent)
|
|
||||||
and norm_pkg_path == _normalize_path(Path(parent_path, *rest))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_path(filename: _Path) -> str:
|
|
||||||
"""Normalize a file/dir name for comparison purposes"""
|
|
||||||
# See pkg_resources.normalize_path
|
|
||||||
file = os.path.abspath(filename) if sys.platform == 'cygwin' else filename
|
|
||||||
return os.path.normcase(os.path.realpath(os.path.normpath(file)))
|
|
||||||
|
|
||||||
|
|
||||||
def _empty_dir(dir_: _P) -> _P:
|
|
||||||
"""Create a directory ensured to be empty. Existing files may be removed."""
|
|
||||||
shutil.rmtree(dir_, ignore_errors=True)
|
|
||||||
os.makedirs(dir_)
|
|
||||||
return dir_
|
|
||||||
|
|
||||||
|
|
||||||
def _make_identifier(name: str) -> str:
|
|
||||||
"""Make a string safe to be used as Python identifier.
|
|
||||||
>>> _make_identifier("12abc")
|
|
||||||
'_12abc'
|
|
||||||
>>> _make_identifier("__editable__.myns.pkg-78.9.3_local")
|
|
||||||
'__editable___myns_pkg_78_9_3_local'
|
|
||||||
"""
|
|
||||||
safe = re.sub(r'\W|^(?=\d)', '_', name)
|
|
||||||
assert safe.isidentifier()
|
|
||||||
return safe
|
|
||||||
|
|
||||||
|
|
||||||
class _NamespaceInstaller(namespaces.Installer):
|
|
||||||
def __init__(self, distribution, installation_dir, editable_name, src_root):
|
|
||||||
self.distribution = distribution
|
|
||||||
self.src_root = src_root
|
|
||||||
self.installation_dir = installation_dir
|
|
||||||
self.editable_name = editable_name
|
|
||||||
self.outputs = []
|
|
||||||
self.dry_run = False
|
|
||||||
|
|
||||||
def _get_target(self):
|
|
||||||
"""Installation target."""
|
|
||||||
return os.path.join(self.installation_dir, self.editable_name)
|
|
||||||
|
|
||||||
def _get_root(self):
|
|
||||||
"""Where the modules/packages should be loaded from."""
|
|
||||||
return repr(str(self.src_root))
|
|
||||||
|
|
||||||
|
|
||||||
_FINDER_TEMPLATE = """\
|
|
||||||
import sys
|
|
||||||
from importlib.machinery import ModuleSpec
|
|
||||||
from importlib.machinery import all_suffixes as module_suffixes
|
|
||||||
from importlib.util import spec_from_file_location
|
|
||||||
from itertools import chain
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
MAPPING = {mapping!r}
|
|
||||||
NAMESPACES = {namespaces!r}
|
|
||||||
PATH_PLACEHOLDER = {name!r} + ".__path_hook__"
|
|
||||||
|
|
||||||
|
|
||||||
class _EditableFinder: # MetaPathFinder
|
|
||||||
@classmethod
|
|
||||||
def find_spec(cls, fullname, path=None, target=None):
|
|
||||||
for pkg, pkg_path in reversed(list(MAPPING.items())):
|
|
||||||
if fullname == pkg or fullname.startswith(f"{{pkg}}."):
|
|
||||||
rest = fullname.replace(pkg, "", 1).strip(".").split(".")
|
|
||||||
return cls._find_spec(fullname, Path(pkg_path, *rest))
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _find_spec(cls, fullname, candidate_path):
|
|
||||||
init = candidate_path / "__init__.py"
|
|
||||||
candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
|
|
||||||
for candidate in chain([init], candidates):
|
|
||||||
if candidate.exists():
|
|
||||||
return spec_from_file_location(fullname, candidate)
|
|
||||||
|
|
||||||
|
|
||||||
class _EditableNamespaceFinder: # PathEntryFinder
|
|
||||||
@classmethod
|
|
||||||
def _path_hook(cls, path):
|
|
||||||
if path == PATH_PLACEHOLDER:
|
|
||||||
return cls
|
|
||||||
raise ImportError
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _paths(cls, fullname):
|
|
||||||
# Ensure __path__ is not empty for the spec to be considered a namespace.
|
|
||||||
return NAMESPACES[fullname] or MAPPING.get(fullname) or [PATH_PLACEHOLDER]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_spec(cls, fullname, target=None):
|
|
||||||
if fullname in NAMESPACES:
|
|
||||||
spec = ModuleSpec(fullname, None, is_package=True)
|
|
||||||
spec.submodule_search_locations = cls._paths(fullname)
|
|
||||||
return spec
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_module(cls, fullname):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def install():
|
|
||||||
if not any(finder == _EditableFinder for finder in sys.meta_path):
|
|
||||||
sys.meta_path.append(_EditableFinder)
|
|
||||||
|
|
||||||
if not NAMESPACES:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):
|
|
||||||
# PathEntryFinder is needed to create NamespaceSpec without private APIS
|
|
||||||
sys.path_hooks.append(_EditableNamespaceFinder._path_hook)
|
|
||||||
if PATH_PLACEHOLDER not in sys.path:
|
|
||||||
sys.path.append(PATH_PLACEHOLDER) # Used just to trigger the path hook
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _finder_template(
|
|
||||||
name: str, mapping: Mapping[str, str], namespaces: Dict[str, List[str]]
|
|
||||||
) -> str:
|
|
||||||
"""Create a string containing the code for the``MetaPathFinder`` and
|
|
||||||
``PathEntryFinder``.
|
|
||||||
"""
|
|
||||||
mapping = dict(sorted(mapping.items(), key=lambda p: p[0]))
|
|
||||||
return _FINDER_TEMPLATE.format(name=name, mapping=mapping, namespaces=namespaces)
|
|
||||||
|
|
||||||
|
|
||||||
class InformationOnly(UserWarning):
|
|
||||||
"""Currently there is no clear way of displaying messages to the users
|
|
||||||
that use the setuptools backend directly via ``pip``.
|
|
||||||
The only thing that might work is a warning, although it is not the
|
|
||||||
most appropriate tool for the job...
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class LinksNotSupported(errors.FileError):
|
|
||||||
"""File system does not seem to support either symlinks or hard links."""
|
|
||||||
@@ -1,775 +0,0 @@
|
|||||||
"""setuptools.command.egg_info
|
|
||||||
|
|
||||||
Create a distribution's .egg-info directory and contents"""
|
|
||||||
|
|
||||||
from distutils.filelist import FileList as _FileList
|
|
||||||
from distutils.errors import DistutilsInternalError
|
|
||||||
from distutils.util import convert_path
|
|
||||||
from distutils import log
|
|
||||||
import distutils.errors
|
|
||||||
import distutils.filelist
|
|
||||||
import functools
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import io
|
|
||||||
import warnings
|
|
||||||
import time
|
|
||||||
import collections
|
|
||||||
|
|
||||||
from .._importlib import metadata
|
|
||||||
from .. import _entry_points
|
|
||||||
|
|
||||||
from setuptools import Command
|
|
||||||
from setuptools.command.sdist import sdist
|
|
||||||
from setuptools.command.sdist import walk_revctrl
|
|
||||||
from setuptools.command.setopt import edit_config
|
|
||||||
from setuptools.command import bdist_egg
|
|
||||||
from pkg_resources import (
|
|
||||||
Requirement, safe_name, parse_version,
|
|
||||||
safe_version, to_filename)
|
|
||||||
import setuptools.unicode_utils as unicode_utils
|
|
||||||
from setuptools.glob import glob
|
|
||||||
|
|
||||||
from setuptools.extern import packaging
|
|
||||||
from setuptools.extern.jaraco.text import yield_lines
|
|
||||||
from setuptools import SetuptoolsDeprecationWarning
|
|
||||||
|
|
||||||
|
|
||||||
def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME
|
|
||||||
"""
|
|
||||||
Translate a file path glob like '*.txt' in to a regular expression.
|
|
||||||
This differs from fnmatch.translate which allows wildcards to match
|
|
||||||
directory separators. It also knows about '**/' which matches any number of
|
|
||||||
directories.
|
|
||||||
"""
|
|
||||||
pat = ''
|
|
||||||
|
|
||||||
# This will split on '/' within [character classes]. This is deliberate.
|
|
||||||
chunks = glob.split(os.path.sep)
|
|
||||||
|
|
||||||
sep = re.escape(os.sep)
|
|
||||||
valid_char = '[^%s]' % (sep,)
|
|
||||||
|
|
||||||
for c, chunk in enumerate(chunks):
|
|
||||||
last_chunk = c == len(chunks) - 1
|
|
||||||
|
|
||||||
# Chunks that are a literal ** are globstars. They match anything.
|
|
||||||
if chunk == '**':
|
|
||||||
if last_chunk:
|
|
||||||
# Match anything if this is the last component
|
|
||||||
pat += '.*'
|
|
||||||
else:
|
|
||||||
# Match '(name/)*'
|
|
||||||
pat += '(?:%s+%s)*' % (valid_char, sep)
|
|
||||||
continue # Break here as the whole path component has been handled
|
|
||||||
|
|
||||||
# Find any special characters in the remainder
|
|
||||||
i = 0
|
|
||||||
chunk_len = len(chunk)
|
|
||||||
while i < chunk_len:
|
|
||||||
char = chunk[i]
|
|
||||||
if char == '*':
|
|
||||||
# Match any number of name characters
|
|
||||||
pat += valid_char + '*'
|
|
||||||
elif char == '?':
|
|
||||||
# Match a name character
|
|
||||||
pat += valid_char
|
|
||||||
elif char == '[':
|
|
||||||
# Character class
|
|
||||||
inner_i = i + 1
|
|
||||||
# Skip initial !/] chars
|
|
||||||
if inner_i < chunk_len and chunk[inner_i] == '!':
|
|
||||||
inner_i = inner_i + 1
|
|
||||||
if inner_i < chunk_len and chunk[inner_i] == ']':
|
|
||||||
inner_i = inner_i + 1
|
|
||||||
|
|
||||||
# Loop till the closing ] is found
|
|
||||||
while inner_i < chunk_len and chunk[inner_i] != ']':
|
|
||||||
inner_i = inner_i + 1
|
|
||||||
|
|
||||||
if inner_i >= chunk_len:
|
|
||||||
# Got to the end of the string without finding a closing ]
|
|
||||||
# Do not treat this as a matching group, but as a literal [
|
|
||||||
pat += re.escape(char)
|
|
||||||
else:
|
|
||||||
# Grab the insides of the [brackets]
|
|
||||||
inner = chunk[i + 1:inner_i]
|
|
||||||
char_class = ''
|
|
||||||
|
|
||||||
# Class negation
|
|
||||||
if inner[0] == '!':
|
|
||||||
char_class = '^'
|
|
||||||
inner = inner[1:]
|
|
||||||
|
|
||||||
char_class += re.escape(inner)
|
|
||||||
pat += '[%s]' % (char_class,)
|
|
||||||
|
|
||||||
# Skip to the end ]
|
|
||||||
i = inner_i
|
|
||||||
else:
|
|
||||||
pat += re.escape(char)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# Join each chunk with the dir separator
|
|
||||||
if not last_chunk:
|
|
||||||
pat += sep
|
|
||||||
|
|
||||||
pat += r'\Z'
|
|
||||||
return re.compile(pat, flags=re.MULTILINE | re.DOTALL)
|
|
||||||
|
|
||||||
|
|
||||||
class InfoCommon:
|
|
||||||
tag_build = None
|
|
||||||
tag_date = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return safe_name(self.distribution.get_name())
|
|
||||||
|
|
||||||
def tagged_version(self):
|
|
||||||
return safe_version(self._maybe_tag(self.distribution.get_version()))
|
|
||||||
|
|
||||||
def _maybe_tag(self, version):
|
|
||||||
"""
|
|
||||||
egg_info may be called more than once for a distribution,
|
|
||||||
in which case the version string already contains all tags.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
version if self.vtags and self._already_tagged(version)
|
|
||||||
else version + self.vtags
|
|
||||||
)
|
|
||||||
|
|
||||||
def _already_tagged(self, version: str) -> bool:
|
|
||||||
# Depending on their format, tags may change with version normalization.
|
|
||||||
# So in addition the regular tags, we have to search for the normalized ones.
|
|
||||||
return version.endswith(self.vtags) or version.endswith(self._safe_tags())
|
|
||||||
|
|
||||||
def _safe_tags(self) -> str:
|
|
||||||
# To implement this we can rely on `safe_version` pretending to be version 0
|
|
||||||
# followed by tags. Then we simply discard the starting 0 (fake version number)
|
|
||||||
return safe_version(f"0{self.vtags}")[1:]
|
|
||||||
|
|
||||||
def tags(self) -> str:
|
|
||||||
version = ''
|
|
||||||
if self.tag_build:
|
|
||||||
version += self.tag_build
|
|
||||||
if self.tag_date:
|
|
||||||
version += time.strftime("%Y%m%d")
|
|
||||||
return version
|
|
||||||
vtags = property(tags)
|
|
||||||
|
|
||||||
|
|
||||||
class egg_info(InfoCommon, Command):
|
|
||||||
description = "create a distribution's .egg-info directory"
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
('egg-base=', 'e', "directory containing .egg-info directories"
|
|
||||||
" (default: top of the source tree)"),
|
|
||||||
('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"),
|
|
||||||
('tag-build=', 'b', "Specify explicit tag to add to version number"),
|
|
||||||
('no-date', 'D', "Don't include date stamp [default]"),
|
|
||||||
]
|
|
||||||
|
|
||||||
boolean_options = ['tag-date']
|
|
||||||
negative_opt = {
|
|
||||||
'no-date': 'tag-date',
|
|
||||||
}
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.egg_base = None
|
|
||||||
self.egg_name = None
|
|
||||||
self.egg_info = None
|
|
||||||
self.egg_version = None
|
|
||||||
self.broken_egg_info = False
|
|
||||||
self.ignore_egg_info_in_manifest = False
|
|
||||||
|
|
||||||
####################################
|
|
||||||
# allow the 'tag_svn_revision' to be detected and
|
|
||||||
# set, supporting sdists built on older Setuptools.
|
|
||||||
@property
|
|
||||||
def tag_svn_revision(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@tag_svn_revision.setter
|
|
||||||
def tag_svn_revision(self, value):
|
|
||||||
pass
|
|
||||||
####################################
|
|
||||||
|
|
||||||
def save_version_info(self, filename):
|
|
||||||
"""
|
|
||||||
Materialize the value of date into the
|
|
||||||
build tag. Install build keys in a deterministic order
|
|
||||||
to avoid arbitrary reordering on subsequent builds.
|
|
||||||
"""
|
|
||||||
egg_info = collections.OrderedDict()
|
|
||||||
# follow the order these keys would have been added
|
|
||||||
# when PYTHONHASHSEED=0
|
|
||||||
egg_info['tag_build'] = self.tags()
|
|
||||||
egg_info['tag_date'] = 0
|
|
||||||
edit_config(filename, dict(egg_info=egg_info))
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
# Note: we need to capture the current value returned
|
|
||||||
# by `self.tagged_version()`, so we can later update
|
|
||||||
# `self.distribution.metadata.version` without
|
|
||||||
# repercussions.
|
|
||||||
self.egg_name = self.name
|
|
||||||
self.egg_version = self.tagged_version()
|
|
||||||
parsed_version = parse_version(self.egg_version)
|
|
||||||
|
|
||||||
try:
|
|
||||||
is_version = isinstance(parsed_version, packaging.version.Version)
|
|
||||||
spec = "%s==%s" if is_version else "%s===%s"
|
|
||||||
Requirement(spec % (self.egg_name, self.egg_version))
|
|
||||||
except ValueError as e:
|
|
||||||
raise distutils.errors.DistutilsOptionError(
|
|
||||||
"Invalid distribution name or version syntax: %s-%s" %
|
|
||||||
(self.egg_name, self.egg_version)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
if self.egg_base is None:
|
|
||||||
dirs = self.distribution.package_dir
|
|
||||||
self.egg_base = (dirs or {}).get('', os.curdir)
|
|
||||||
|
|
||||||
self.ensure_dirname('egg_base')
|
|
||||||
self.egg_info = to_filename(self.egg_name) + '.egg-info'
|
|
||||||
if self.egg_base != os.curdir:
|
|
||||||
self.egg_info = os.path.join(self.egg_base, self.egg_info)
|
|
||||||
if '-' in self.egg_name:
|
|
||||||
self.check_broken_egg_info()
|
|
||||||
|
|
||||||
# Set package version for the benefit of dumber commands
|
|
||||||
# (e.g. sdist, bdist_wininst, etc.)
|
|
||||||
#
|
|
||||||
self.distribution.metadata.version = self.egg_version
|
|
||||||
|
|
||||||
# If we bootstrapped around the lack of a PKG-INFO, as might be the
|
|
||||||
# case in a fresh checkout, make sure that any special tags get added
|
|
||||||
# to the version info
|
|
||||||
#
|
|
||||||
pd = self.distribution._patched_dist
|
|
||||||
if pd is not None and pd.key == self.egg_name.lower():
|
|
||||||
pd._version = self.egg_version
|
|
||||||
pd._parsed_version = parse_version(self.egg_version)
|
|
||||||
self.distribution._patched_dist = None
|
|
||||||
|
|
||||||
def write_or_delete_file(self, what, filename, data, force=False):
|
|
||||||
"""Write `data` to `filename` or delete if empty
|
|
||||||
|
|
||||||
If `data` is non-empty, this routine is the same as ``write_file()``.
|
|
||||||
If `data` is empty but not ``None``, this is the same as calling
|
|
||||||
``delete_file(filename)`. If `data` is ``None``, then this is a no-op
|
|
||||||
unless `filename` exists, in which case a warning is issued about the
|
|
||||||
orphaned file (if `force` is false), or deleted (if `force` is true).
|
|
||||||
"""
|
|
||||||
if data:
|
|
||||||
self.write_file(what, filename, data)
|
|
||||||
elif os.path.exists(filename):
|
|
||||||
if data is None and not force:
|
|
||||||
log.warn(
|
|
||||||
"%s not set in setup(), but %s exists", what, filename
|
|
||||||
)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.delete_file(filename)
|
|
||||||
|
|
||||||
def write_file(self, what, filename, data):
|
|
||||||
"""Write `data` to `filename` (if not a dry run) after announcing it
|
|
||||||
|
|
||||||
`what` is used in a log message to identify what is being written
|
|
||||||
to the file.
|
|
||||||
"""
|
|
||||||
log.info("writing %s to %s", what, filename)
|
|
||||||
data = data.encode("utf-8")
|
|
||||||
if not self.dry_run:
|
|
||||||
f = open(filename, 'wb')
|
|
||||||
f.write(data)
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def delete_file(self, filename):
|
|
||||||
"""Delete `filename` (if not a dry run) after announcing it"""
|
|
||||||
log.info("deleting %s", filename)
|
|
||||||
if not self.dry_run:
|
|
||||||
os.unlink(filename)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.mkpath(self.egg_info)
|
|
||||||
try:
|
|
||||||
os.utime(self.egg_info, None)
|
|
||||||
except OSError as e:
|
|
||||||
msg = f"Cannot update time stamp of directory '{self.egg_info}'"
|
|
||||||
raise distutils.errors.DistutilsFileError(msg) from e
|
|
||||||
for ep in metadata.entry_points(group='egg_info.writers'):
|
|
||||||
writer = ep.load()
|
|
||||||
writer(self, ep.name, os.path.join(self.egg_info, ep.name))
|
|
||||||
|
|
||||||
# Get rid of native_libs.txt if it was put there by older bdist_egg
|
|
||||||
nl = os.path.join(self.egg_info, "native_libs.txt")
|
|
||||||
if os.path.exists(nl):
|
|
||||||
self.delete_file(nl)
|
|
||||||
|
|
||||||
self.find_sources()
|
|
||||||
|
|
||||||
def find_sources(self):
|
|
||||||
"""Generate SOURCES.txt manifest file"""
|
|
||||||
manifest_filename = os.path.join(self.egg_info, "SOURCES.txt")
|
|
||||||
mm = manifest_maker(self.distribution)
|
|
||||||
mm.ignore_egg_info_dir = self.ignore_egg_info_in_manifest
|
|
||||||
mm.manifest = manifest_filename
|
|
||||||
mm.run()
|
|
||||||
self.filelist = mm.filelist
|
|
||||||
|
|
||||||
def check_broken_egg_info(self):
|
|
||||||
bei = self.egg_name + '.egg-info'
|
|
||||||
if self.egg_base != os.curdir:
|
|
||||||
bei = os.path.join(self.egg_base, bei)
|
|
||||||
if os.path.exists(bei):
|
|
||||||
log.warn(
|
|
||||||
"-" * 78 + '\n'
|
|
||||||
"Note: Your current .egg-info directory has a '-' in its name;"
|
|
||||||
'\nthis will not work correctly with "setup.py develop".\n\n'
|
|
||||||
'Please rename %s to %s to correct this problem.\n' + '-' * 78,
|
|
||||||
bei, self.egg_info
|
|
||||||
)
|
|
||||||
self.broken_egg_info = self.egg_info
|
|
||||||
self.egg_info = bei # make it work for now
|
|
||||||
|
|
||||||
|
|
||||||
class FileList(_FileList):
|
|
||||||
# Implementations of the various MANIFEST.in commands
|
|
||||||
|
|
||||||
def __init__(self, warn=None, debug_print=None, ignore_egg_info_dir=False):
|
|
||||||
super().__init__(warn, debug_print)
|
|
||||||
self.ignore_egg_info_dir = ignore_egg_info_dir
|
|
||||||
|
|
||||||
def process_template_line(self, line):
|
|
||||||
# Parse the line: split it up, make sure the right number of words
|
|
||||||
# is there, and return the relevant words. 'action' is always
|
|
||||||
# defined: it's the first word of the line. Which of the other
|
|
||||||
# three are defined depends on the action; it'll be either
|
|
||||||
# patterns, (dir and patterns), or (dir_pattern).
|
|
||||||
(action, patterns, dir, dir_pattern) = self._parse_template_line(line)
|
|
||||||
|
|
||||||
action_map = {
|
|
||||||
'include': self.include,
|
|
||||||
'exclude': self.exclude,
|
|
||||||
'global-include': self.global_include,
|
|
||||||
'global-exclude': self.global_exclude,
|
|
||||||
'recursive-include': functools.partial(
|
|
||||||
self.recursive_include, dir,
|
|
||||||
),
|
|
||||||
'recursive-exclude': functools.partial(
|
|
||||||
self.recursive_exclude, dir,
|
|
||||||
),
|
|
||||||
'graft': self.graft,
|
|
||||||
'prune': self.prune,
|
|
||||||
}
|
|
||||||
log_map = {
|
|
||||||
'include': "warning: no files found matching '%s'",
|
|
||||||
'exclude': (
|
|
||||||
"warning: no previously-included files found "
|
|
||||||
"matching '%s'"
|
|
||||||
),
|
|
||||||
'global-include': (
|
|
||||||
"warning: no files found matching '%s' "
|
|
||||||
"anywhere in distribution"
|
|
||||||
),
|
|
||||||
'global-exclude': (
|
|
||||||
"warning: no previously-included files matching "
|
|
||||||
"'%s' found anywhere in distribution"
|
|
||||||
),
|
|
||||||
'recursive-include': (
|
|
||||||
"warning: no files found matching '%s' "
|
|
||||||
"under directory '%s'"
|
|
||||||
),
|
|
||||||
'recursive-exclude': (
|
|
||||||
"warning: no previously-included files matching "
|
|
||||||
"'%s' found under directory '%s'"
|
|
||||||
),
|
|
||||||
'graft': "warning: no directories found matching '%s'",
|
|
||||||
'prune': "no previously-included directories found matching '%s'",
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
process_action = action_map[action]
|
|
||||||
except KeyError:
|
|
||||||
raise DistutilsInternalError(
|
|
||||||
"this cannot happen: invalid action '{action!s}'".
|
|
||||||
format(action=action),
|
|
||||||
)
|
|
||||||
|
|
||||||
# OK, now we know that the action is valid and we have the
|
|
||||||
# right number of words on the line for that action -- so we
|
|
||||||
# can proceed with minimal error-checking.
|
|
||||||
|
|
||||||
action_is_recursive = action.startswith('recursive-')
|
|
||||||
if action in {'graft', 'prune'}:
|
|
||||||
patterns = [dir_pattern]
|
|
||||||
extra_log_args = (dir, ) if action_is_recursive else ()
|
|
||||||
log_tmpl = log_map[action]
|
|
||||||
|
|
||||||
self.debug_print(
|
|
||||||
' '.join(
|
|
||||||
[action] +
|
|
||||||
([dir] if action_is_recursive else []) +
|
|
||||||
patterns,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for pattern in patterns:
|
|
||||||
if not process_action(pattern):
|
|
||||||
log.warn(log_tmpl, pattern, *extra_log_args)
|
|
||||||
|
|
||||||
def _remove_files(self, predicate):
|
|
||||||
"""
|
|
||||||
Remove all files from the file list that match the predicate.
|
|
||||||
Return True if any matching files were removed
|
|
||||||
"""
|
|
||||||
found = False
|
|
||||||
for i in range(len(self.files) - 1, -1, -1):
|
|
||||||
if predicate(self.files[i]):
|
|
||||||
self.debug_print(" removing " + self.files[i])
|
|
||||||
del self.files[i]
|
|
||||||
found = True
|
|
||||||
return found
|
|
||||||
|
|
||||||
def include(self, pattern):
|
|
||||||
"""Include files that match 'pattern'."""
|
|
||||||
found = [f for f in glob(pattern) if not os.path.isdir(f)]
|
|
||||||
self.extend(found)
|
|
||||||
return bool(found)
|
|
||||||
|
|
||||||
def exclude(self, pattern):
|
|
||||||
"""Exclude files that match 'pattern'."""
|
|
||||||
match = translate_pattern(pattern)
|
|
||||||
return self._remove_files(match.match)
|
|
||||||
|
|
||||||
def recursive_include(self, dir, pattern):
|
|
||||||
"""
|
|
||||||
Include all files anywhere in 'dir/' that match the pattern.
|
|
||||||
"""
|
|
||||||
full_pattern = os.path.join(dir, '**', pattern)
|
|
||||||
found = [f for f in glob(full_pattern, recursive=True)
|
|
||||||
if not os.path.isdir(f)]
|
|
||||||
self.extend(found)
|
|
||||||
return bool(found)
|
|
||||||
|
|
||||||
def recursive_exclude(self, dir, pattern):
|
|
||||||
"""
|
|
||||||
Exclude any file anywhere in 'dir/' that match the pattern.
|
|
||||||
"""
|
|
||||||
match = translate_pattern(os.path.join(dir, '**', pattern))
|
|
||||||
return self._remove_files(match.match)
|
|
||||||
|
|
||||||
def graft(self, dir):
|
|
||||||
"""Include all files from 'dir/'."""
|
|
||||||
found = [
|
|
||||||
item
|
|
||||||
for match_dir in glob(dir)
|
|
||||||
for item in distutils.filelist.findall(match_dir)
|
|
||||||
]
|
|
||||||
self.extend(found)
|
|
||||||
return bool(found)
|
|
||||||
|
|
||||||
def prune(self, dir):
|
|
||||||
"""Filter out files from 'dir/'."""
|
|
||||||
match = translate_pattern(os.path.join(dir, '**'))
|
|
||||||
return self._remove_files(match.match)
|
|
||||||
|
|
||||||
def global_include(self, pattern):
|
|
||||||
"""
|
|
||||||
Include all files anywhere in the current directory that match the
|
|
||||||
pattern. This is very inefficient on large file trees.
|
|
||||||
"""
|
|
||||||
if self.allfiles is None:
|
|
||||||
self.findall()
|
|
||||||
match = translate_pattern(os.path.join('**', pattern))
|
|
||||||
found = [f for f in self.allfiles if match.match(f)]
|
|
||||||
self.extend(found)
|
|
||||||
return bool(found)
|
|
||||||
|
|
||||||
def global_exclude(self, pattern):
|
|
||||||
"""
|
|
||||||
Exclude all files anywhere that match the pattern.
|
|
||||||
"""
|
|
||||||
match = translate_pattern(os.path.join('**', pattern))
|
|
||||||
return self._remove_files(match.match)
|
|
||||||
|
|
||||||
def append(self, item):
|
|
||||||
if item.endswith('\r'): # Fix older sdists built on Windows
|
|
||||||
item = item[:-1]
|
|
||||||
path = convert_path(item)
|
|
||||||
|
|
||||||
if self._safe_path(path):
|
|
||||||
self.files.append(path)
|
|
||||||
|
|
||||||
def extend(self, paths):
|
|
||||||
self.files.extend(filter(self._safe_path, paths))
|
|
||||||
|
|
||||||
def _repair(self):
|
|
||||||
"""
|
|
||||||
Replace self.files with only safe paths
|
|
||||||
|
|
||||||
Because some owners of FileList manipulate the underlying
|
|
||||||
``files`` attribute directly, this method must be called to
|
|
||||||
repair those paths.
|
|
||||||
"""
|
|
||||||
self.files = list(filter(self._safe_path, self.files))
|
|
||||||
|
|
||||||
def _safe_path(self, path):
|
|
||||||
enc_warn = "'%s' not %s encodable -- skipping"
|
|
||||||
|
|
||||||
# To avoid accidental trans-codings errors, first to unicode
|
|
||||||
u_path = unicode_utils.filesys_decode(path)
|
|
||||||
if u_path is None:
|
|
||||||
log.warn("'%s' in unexpected encoding -- skipping" % path)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Must ensure utf-8 encodability
|
|
||||||
utf8_path = unicode_utils.try_encode(u_path, "utf-8")
|
|
||||||
if utf8_path is None:
|
|
||||||
log.warn(enc_warn, path, 'utf-8')
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
# ignore egg-info paths
|
|
||||||
is_egg_info = ".egg-info" in u_path or b".egg-info" in utf8_path
|
|
||||||
if self.ignore_egg_info_dir and is_egg_info:
|
|
||||||
return False
|
|
||||||
# accept is either way checks out
|
|
||||||
if os.path.exists(u_path) or os.path.exists(utf8_path):
|
|
||||||
return True
|
|
||||||
# this will catch any encode errors decoding u_path
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
log.warn(enc_warn, path, sys.getfilesystemencoding())
|
|
||||||
|
|
||||||
|
|
||||||
class manifest_maker(sdist):
|
|
||||||
template = "MANIFEST.in"
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.use_defaults = 1
|
|
||||||
self.prune = 1
|
|
||||||
self.manifest_only = 1
|
|
||||||
self.force_manifest = 1
|
|
||||||
self.ignore_egg_info_dir = False
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir)
|
|
||||||
if not os.path.exists(self.manifest):
|
|
||||||
self.write_manifest() # it must exist so it'll get in the list
|
|
||||||
self.add_defaults()
|
|
||||||
if os.path.exists(self.template):
|
|
||||||
self.read_template()
|
|
||||||
self.add_license_files()
|
|
||||||
self._add_referenced_files()
|
|
||||||
self.prune_file_list()
|
|
||||||
self.filelist.sort()
|
|
||||||
self.filelist.remove_duplicates()
|
|
||||||
self.write_manifest()
|
|
||||||
|
|
||||||
def _manifest_normalize(self, path):
|
|
||||||
path = unicode_utils.filesys_decode(path)
|
|
||||||
return path.replace(os.sep, '/')
|
|
||||||
|
|
||||||
def write_manifest(self):
|
|
||||||
"""
|
|
||||||
Write the file list in 'self.filelist' to the manifest file
|
|
||||||
named by 'self.manifest'.
|
|
||||||
"""
|
|
||||||
self.filelist._repair()
|
|
||||||
|
|
||||||
# Now _repairs should encodability, but not unicode
|
|
||||||
files = [self._manifest_normalize(f) for f in self.filelist.files]
|
|
||||||
msg = "writing manifest file '%s'" % self.manifest
|
|
||||||
self.execute(write_file, (self.manifest, files), msg)
|
|
||||||
|
|
||||||
def warn(self, msg):
|
|
||||||
if not self._should_suppress_warning(msg):
|
|
||||||
sdist.warn(self, msg)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _should_suppress_warning(msg):
|
|
||||||
"""
|
|
||||||
suppress missing-file warnings from sdist
|
|
||||||
"""
|
|
||||||
return re.match(r"standard file .*not found", msg)
|
|
||||||
|
|
||||||
def add_defaults(self):
|
|
||||||
sdist.add_defaults(self)
|
|
||||||
self.filelist.append(self.template)
|
|
||||||
self.filelist.append(self.manifest)
|
|
||||||
rcfiles = list(walk_revctrl())
|
|
||||||
if rcfiles:
|
|
||||||
self.filelist.extend(rcfiles)
|
|
||||||
elif os.path.exists(self.manifest):
|
|
||||||
self.read_manifest()
|
|
||||||
|
|
||||||
if os.path.exists("setup.py"):
|
|
||||||
# setup.py should be included by default, even if it's not
|
|
||||||
# the script called to create the sdist
|
|
||||||
self.filelist.append("setup.py")
|
|
||||||
|
|
||||||
ei_cmd = self.get_finalized_command('egg_info')
|
|
||||||
self.filelist.graft(ei_cmd.egg_info)
|
|
||||||
|
|
||||||
def add_license_files(self):
|
|
||||||
license_files = self.distribution.metadata.license_files or []
|
|
||||||
for lf in license_files:
|
|
||||||
log.info("adding license file '%s'", lf)
|
|
||||||
self.filelist.extend(license_files)
|
|
||||||
|
|
||||||
def _add_referenced_files(self):
|
|
||||||
"""Add files referenced by the config (e.g. `file:` directive) to filelist"""
|
|
||||||
referenced = getattr(self.distribution, '_referenced_files', [])
|
|
||||||
# ^-- fallback if dist comes from distutils or is a custom class
|
|
||||||
for rf in referenced:
|
|
||||||
log.debug("adding file referenced by config '%s'", rf)
|
|
||||||
self.filelist.extend(referenced)
|
|
||||||
|
|
||||||
def prune_file_list(self):
|
|
||||||
build = self.get_finalized_command('build')
|
|
||||||
base_dir = self.distribution.get_fullname()
|
|
||||||
self.filelist.prune(build.build_base)
|
|
||||||
self.filelist.prune(base_dir)
|
|
||||||
sep = re.escape(os.sep)
|
|
||||||
self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep,
|
|
||||||
is_regex=1)
|
|
||||||
|
|
||||||
def _safe_data_files(self, build_py):
|
|
||||||
"""
|
|
||||||
The parent class implementation of this method
|
|
||||||
(``sdist``) will try to include data files, which
|
|
||||||
might cause recursion problems when
|
|
||||||
``include_package_data=True``.
|
|
||||||
|
|
||||||
Therefore, avoid triggering any attempt of
|
|
||||||
analyzing/building the manifest again.
|
|
||||||
"""
|
|
||||||
if hasattr(build_py, 'get_data_files_without_manifest'):
|
|
||||||
return build_py.get_data_files_without_manifest()
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"Custom 'build_py' does not implement "
|
|
||||||
"'get_data_files_without_manifest'.\nPlease extend command classes"
|
|
||||||
" from setuptools instead of distutils.",
|
|
||||||
SetuptoolsDeprecationWarning
|
|
||||||
)
|
|
||||||
return build_py.get_data_files()
|
|
||||||
|
|
||||||
|
|
||||||
def write_file(filename, contents):
|
|
||||||
"""Create a file with the specified name and write 'contents' (a
|
|
||||||
sequence of strings without line terminators) to it.
|
|
||||||
"""
|
|
||||||
contents = "\n".join(contents)
|
|
||||||
|
|
||||||
# assuming the contents has been vetted for utf-8 encoding
|
|
||||||
contents = contents.encode("utf-8")
|
|
||||||
|
|
||||||
with open(filename, "wb") as f: # always write POSIX-style manifest
|
|
||||||
f.write(contents)
|
|
||||||
|
|
||||||
|
|
||||||
def write_pkg_info(cmd, basename, filename):
|
|
||||||
log.info("writing %s", filename)
|
|
||||||
if not cmd.dry_run:
|
|
||||||
metadata = cmd.distribution.metadata
|
|
||||||
metadata.version, oldver = cmd.egg_version, metadata.version
|
|
||||||
metadata.name, oldname = cmd.egg_name, metadata.name
|
|
||||||
|
|
||||||
try:
|
|
||||||
# write unescaped data to PKG-INFO, so older pkg_resources
|
|
||||||
# can still parse it
|
|
||||||
metadata.write_pkg_info(cmd.egg_info)
|
|
||||||
finally:
|
|
||||||
metadata.name, metadata.version = oldname, oldver
|
|
||||||
|
|
||||||
safe = getattr(cmd.distribution, 'zip_safe', None)
|
|
||||||
|
|
||||||
bdist_egg.write_safety_flag(cmd.egg_info, safe)
|
|
||||||
|
|
||||||
|
|
||||||
def warn_depends_obsolete(cmd, basename, filename):
|
|
||||||
if os.path.exists(filename):
|
|
||||||
log.warn(
|
|
||||||
"WARNING: 'depends.txt' is not used by setuptools 0.6!\n"
|
|
||||||
"Use the install_requires/extras_require setup() args instead."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _write_requirements(stream, reqs):
|
|
||||||
lines = yield_lines(reqs or ())
|
|
||||||
|
|
||||||
def append_cr(line):
|
|
||||||
return line + '\n'
|
|
||||||
lines = map(append_cr, sorted(lines))
|
|
||||||
stream.writelines(lines)
|
|
||||||
|
|
||||||
|
|
||||||
def write_requirements(cmd, basename, filename):
|
|
||||||
dist = cmd.distribution
|
|
||||||
data = io.StringIO()
|
|
||||||
_write_requirements(data, dist.install_requires)
|
|
||||||
extras_require = dist.extras_require or {}
|
|
||||||
for extra in sorted(extras_require):
|
|
||||||
data.write('\n[{extra}]\n'.format(**vars()))
|
|
||||||
_write_requirements(data, extras_require[extra])
|
|
||||||
cmd.write_or_delete_file("requirements", filename, data.getvalue())
|
|
||||||
|
|
||||||
|
|
||||||
def write_setup_requirements(cmd, basename, filename):
|
|
||||||
data = io.StringIO()
|
|
||||||
_write_requirements(data, cmd.distribution.setup_requires)
|
|
||||||
cmd.write_or_delete_file("setup-requirements", filename, data.getvalue())
|
|
||||||
|
|
||||||
|
|
||||||
def write_toplevel_names(cmd, basename, filename):
|
|
||||||
pkgs = dict.fromkeys(
|
|
||||||
[
|
|
||||||
k.split('.', 1)[0]
|
|
||||||
for k in cmd.distribution.iter_distribution_names()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n')
|
|
||||||
|
|
||||||
|
|
||||||
def overwrite_arg(cmd, basename, filename):
|
|
||||||
write_arg(cmd, basename, filename, True)
|
|
||||||
|
|
||||||
|
|
||||||
def write_arg(cmd, basename, filename, force=False):
|
|
||||||
argname = os.path.splitext(basename)[0]
|
|
||||||
value = getattr(cmd.distribution, argname, None)
|
|
||||||
if value is not None:
|
|
||||||
value = '\n'.join(value) + '\n'
|
|
||||||
cmd.write_or_delete_file(argname, filename, value, force)
|
|
||||||
|
|
||||||
|
|
||||||
def write_entries(cmd, basename, filename):
|
|
||||||
eps = _entry_points.load(cmd.distribution.entry_points)
|
|
||||||
defn = _entry_points.render(eps)
|
|
||||||
cmd.write_or_delete_file('entry points', filename, defn, True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pkg_info_revision():
|
|
||||||
"""
|
|
||||||
Get a -r### off of PKG-INFO Version in case this is an sdist of
|
|
||||||
a subversion revision.
|
|
||||||
"""
|
|
||||||
warnings.warn(
|
|
||||||
"get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning)
|
|
||||||
if os.path.exists('PKG-INFO'):
|
|
||||||
with io.open('PKG-INFO') as f:
|
|
||||||
for line in f:
|
|
||||||
match = re.match(r"Version:.*-r(\d+)\s*$", line)
|
|
||||||
if match:
|
|
||||||
return int(match.group(1))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):
|
|
||||||
"""Deprecated behavior warning for EggInfo, bypassing suppression."""
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
from distutils.errors import DistutilsArgError
|
|
||||||
import inspect
|
|
||||||
import glob
|
|
||||||
import warnings
|
|
||||||
import platform
|
|
||||||
import distutils.command.install as orig
|
|
||||||
|
|
||||||
import setuptools
|
|
||||||
|
|
||||||
# Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for
|
|
||||||
# now. See https://github.com/pypa/setuptools/issues/199/
|
|
||||||
_install = orig.install
|
|
||||||
|
|
||||||
|
|
||||||
class install(orig.install):
|
|
||||||
"""Use easy_install to install the package, w/dependencies"""
|
|
||||||
|
|
||||||
user_options = orig.install.user_options + [
|
|
||||||
('old-and-unmanageable', None, "Try not to use this!"),
|
|
||||||
('single-version-externally-managed', None,
|
|
||||||
"used by system package builders to create 'flat' eggs"),
|
|
||||||
]
|
|
||||||
boolean_options = orig.install.boolean_options + [
|
|
||||||
'old-and-unmanageable', 'single-version-externally-managed',
|
|
||||||
]
|
|
||||||
new_commands = [
|
|
||||||
('install_egg_info', lambda self: True),
|
|
||||||
('install_scripts', lambda self: True),
|
|
||||||
]
|
|
||||||
_nc = dict(new_commands)
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
|
|
||||||
warnings.warn(
|
|
||||||
"setup.py install is deprecated. "
|
|
||||||
"Use build and pip and other standards-based tools.",
|
|
||||||
setuptools.SetuptoolsDeprecationWarning,
|
|
||||||
)
|
|
||||||
|
|
||||||
orig.install.initialize_options(self)
|
|
||||||
self.old_and_unmanageable = None
|
|
||||||
self.single_version_externally_managed = None
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
orig.install.finalize_options(self)
|
|
||||||
if self.root:
|
|
||||||
self.single_version_externally_managed = True
|
|
||||||
elif self.single_version_externally_managed:
|
|
||||||
if not self.root and not self.record:
|
|
||||||
raise DistutilsArgError(
|
|
||||||
"You must specify --record or --root when building system"
|
|
||||||
" packages"
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_extra_path(self):
|
|
||||||
if self.root or self.single_version_externally_managed:
|
|
||||||
# explicit backward-compatibility mode, allow extra_path to work
|
|
||||||
return orig.install.handle_extra_path(self)
|
|
||||||
|
|
||||||
# Ignore extra_path when installing an egg (or being run by another
|
|
||||||
# command without --root or --single-version-externally-managed
|
|
||||||
self.path_file = None
|
|
||||||
self.extra_dirs = ''
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
# Explicit request for old-style install? Just do it
|
|
||||||
if self.old_and_unmanageable or self.single_version_externally_managed:
|
|
||||||
return orig.install.run(self)
|
|
||||||
|
|
||||||
if not self._called_from_setup(inspect.currentframe()):
|
|
||||||
# Run in backward-compatibility mode to support bdist_* commands.
|
|
||||||
orig.install.run(self)
|
|
||||||
else:
|
|
||||||
self.do_egg_install()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _called_from_setup(run_frame):
|
|
||||||
"""
|
|
||||||
Attempt to detect whether run() was called from setup() or by another
|
|
||||||
command. If called by setup(), the parent caller will be the
|
|
||||||
'run_command' method in 'distutils.dist', and *its* caller will be
|
|
||||||
the 'run_commands' method. If called any other way, the
|
|
||||||
immediate caller *might* be 'run_command', but it won't have been
|
|
||||||
called by 'run_commands'. Return True in that case or if a call stack
|
|
||||||
is unavailable. Return False otherwise.
|
|
||||||
"""
|
|
||||||
if run_frame is None:
|
|
||||||
msg = "Call stack not available. bdist_* commands may fail."
|
|
||||||
warnings.warn(msg)
|
|
||||||
if platform.python_implementation() == 'IronPython':
|
|
||||||
msg = "For best results, pass -X:Frames to enable call stack."
|
|
||||||
warnings.warn(msg)
|
|
||||||
return True
|
|
||||||
|
|
||||||
frames = inspect.getouterframes(run_frame)
|
|
||||||
for frame in frames[2:4]:
|
|
||||||
caller, = frame[:1]
|
|
||||||
info = inspect.getframeinfo(caller)
|
|
||||||
caller_module = caller.f_globals.get('__name__', '')
|
|
||||||
|
|
||||||
if caller_module == "setuptools.dist" and info.function == "run_command":
|
|
||||||
# Starting from v61.0.0 setuptools overwrites dist.run_command
|
|
||||||
continue
|
|
||||||
|
|
||||||
return (
|
|
||||||
caller_module == 'distutils.dist'
|
|
||||||
and info.function == 'run_commands'
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_egg_install(self):
|
|
||||||
|
|
||||||
easy_install = self.distribution.get_command_class('easy_install')
|
|
||||||
|
|
||||||
cmd = easy_install(
|
|
||||||
self.distribution, args="x", root=self.root, record=self.record,
|
|
||||||
)
|
|
||||||
cmd.ensure_finalized() # finalize before bdist_egg munges install cmd
|
|
||||||
cmd.always_copy_from = '.' # make sure local-dir eggs get installed
|
|
||||||
|
|
||||||
# pick up setup-dir .egg files only: no .egg-info
|
|
||||||
cmd.package_index.scan(glob.glob('*.egg'))
|
|
||||||
|
|
||||||
self.run_command('bdist_egg')
|
|
||||||
args = [self.distribution.get_command_obj('bdist_egg').egg_output]
|
|
||||||
|
|
||||||
if setuptools.bootstrap_install_from:
|
|
||||||
# Bootstrap self-installation of setuptools
|
|
||||||
args.insert(0, setuptools.bootstrap_install_from)
|
|
||||||
|
|
||||||
cmd.args = args
|
|
||||||
cmd.run(show_deprecation=False)
|
|
||||||
setuptools.bootstrap_install_from = None
|
|
||||||
|
|
||||||
|
|
||||||
# XXX Python 3.1 doesn't see _nc if this is inside the class
|
|
||||||
install.sub_commands = (
|
|
||||||
[cmd for cmd in orig.install.sub_commands if cmd[0] not in install._nc] +
|
|
||||||
install.new_commands
|
|
||||||
)
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from distutils import log, dir_util
|
|
||||||
import os, sys
|
|
||||||
|
|
||||||
from setuptools import Command
|
|
||||||
from setuptools import namespaces
|
|
||||||
from setuptools.archive_util import unpack_archive
|
|
||||||
from .._path import ensure_directory
|
|
||||||
import pkg_resources
|
|
||||||
|
|
||||||
|
|
||||||
class install_egg_info(namespaces.Installer, Command):
|
|
||||||
"""Install an .egg-info directory for the package"""
|
|
||||||
|
|
||||||
description = "Install an .egg-info directory for the package"
|
|
||||||
|
|
||||||
user_options = [
|
|
||||||
('install-dir=', 'd', "directory to install to"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
self.install_dir = None
|
|
||||||
self.install_layout = None
|
|
||||||
self.prefix_option = None
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
self.set_undefined_options('install_lib',
|
|
||||||
('install_dir', 'install_dir'))
|
|
||||||
self.set_undefined_options('install',('install_layout','install_layout'))
|
|
||||||
if sys.hexversion > 0x2060000:
|
|
||||||
self.set_undefined_options('install',('prefix_option','prefix_option'))
|
|
||||||
ei_cmd = self.get_finalized_command("egg_info")
|
|
||||||
basename = pkg_resources.Distribution(
|
|
||||||
None, None, ei_cmd.egg_name, ei_cmd.egg_version
|
|
||||||
).egg_name() + '.egg-info'
|
|
||||||
|
|
||||||
if self.install_layout:
|
|
||||||
if not self.install_layout.lower() in ['deb']:
|
|
||||||
raise DistutilsOptionError("unknown value for --install-layout")
|
|
||||||
self.install_layout = self.install_layout.lower()
|
|
||||||
basename = basename.replace('-py%s' % pkg_resources.PY_MAJOR, '')
|
|
||||||
elif self.prefix_option or 'real_prefix' in sys.__dict__:
|
|
||||||
# don't modify for virtualenv
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
basename = basename.replace('-py%s' % pkg_resources.PY_MAJOR, '')
|
|
||||||
|
|
||||||
self.source = ei_cmd.egg_info
|
|
||||||
self.target = os.path.join(self.install_dir, basename)
|
|
||||||
self.outputs = []
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.run_command('egg_info')
|
|
||||||
if os.path.isdir(self.target) and not os.path.islink(self.target):
|
|
||||||
dir_util.remove_tree(self.target, dry_run=self.dry_run)
|
|
||||||
elif os.path.exists(self.target):
|
|
||||||
self.execute(os.unlink, (self.target,), "Removing " + self.target)
|
|
||||||
if not self.dry_run:
|
|
||||||
ensure_directory(self.target)
|
|
||||||
self.execute(
|
|
||||||
self.copytree, (), "Copying %s to %s" % (self.source, self.target)
|
|
||||||
)
|
|
||||||
self.install_namespaces()
|
|
||||||
|
|
||||||
def get_outputs(self):
|
|
||||||
return self.outputs
|
|
||||||
|
|
||||||
def copytree(self):
|
|
||||||
# Copy the .egg-info tree to site-packages
|
|
||||||
def skimmer(src, dst):
|
|
||||||
# filter out source-control directories; note that 'src' is always
|
|
||||||
# a '/'-separated path, regardless of platform. 'dst' is a
|
|
||||||
# platform-specific path.
|
|
||||||
for skip in '.svn/', 'CVS/':
|
|
||||||
if src.startswith(skip) or '/' + skip in src:
|
|
||||||
return None
|
|
||||||
if self.install_layout and self.install_layout in ['deb'] and src.startswith('SOURCES.txt'):
|
|
||||||
log.info("Skipping SOURCES.txt")
|
|
||||||
return None
|
|
||||||
self.outputs.append(dst)
|
|
||||||
log.debug("Copying %s to %s", src, dst)
|
|
||||||
return dst
|
|
||||||
|
|
||||||
unpack_archive(self.source, self.target, skimmer)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from itertools import product, starmap
|
|
||||||
import distutils.command.install_lib as orig
|
|
||||||
|
|
||||||
|
|
||||||
class install_lib(orig.install_lib):
|
|
||||||
"""Don't add compiled flags to filenames of non-Python files"""
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
orig.install_lib.initialize_options(self)
|
|
||||||
self.multiarch = None
|
|
||||||
self.install_layout = None
|
|
||||||
|
|
||||||
def finalize_options(self):
|
|
||||||
orig.install_lib.finalize_options(self)
|
|
||||||
self.set_undefined_options('install',('install_layout','install_layout'))
|
|
||||||
if self.install_layout == 'deb' and sys.version_info[:2] >= (3, 3):
|
|
||||||
import sysconfig
|
|
||||||
self.multiarch = sysconfig.get_config_var('MULTIARCH')
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.build()
|
|
||||||
outfiles = self.install()
|
|
||||||
if outfiles is not None:
|
|
||||||
# always compile, in case we have any extension stubs to deal with
|
|
||||||
self.byte_compile(outfiles)
|
|
||||||
|
|
||||||
def get_exclusions(self):
|
|
||||||
"""
|
|
||||||
Return a collections.Sized collections.Container of paths to be
|
|
||||||
excluded for single_version_externally_managed installations.
|
|
||||||
"""
|
|
||||||
all_packages = (
|
|
||||||
pkg
|
|
||||||
for ns_pkg in self._get_SVEM_NSPs()
|
|
||||||
for pkg in self._all_packages(ns_pkg)
|
|
||||||
)
|
|
||||||
|
|
||||||
excl_specs = product(all_packages, self._gen_exclusion_paths())
|
|
||||||
return set(starmap(self._exclude_pkg_path, excl_specs))
|
|
||||||
|
|
||||||
def _exclude_pkg_path(self, pkg, exclusion_path):
|
|
||||||
"""
|
|
||||||
Given a package name and exclusion path within that package,
|
|
||||||
compute the full exclusion path.
|
|
||||||
"""
|
|
||||||
parts = pkg.split('.') + [exclusion_path]
|
|
||||||
return os.path.join(self.install_dir, *parts)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _all_packages(pkg_name):
|
|
||||||
"""
|
|
||||||
>>> list(install_lib._all_packages('foo.bar.baz'))
|
|
||||||
['foo.bar.baz', 'foo.bar', 'foo']
|
|
||||||
"""
|
|
||||||
while pkg_name:
|
|
||||||
yield pkg_name
|
|
||||||
pkg_name, sep, child = pkg_name.rpartition('.')
|
|
||||||
|
|
||||||
def _get_SVEM_NSPs(self):
|
|
||||||
"""
|
|
||||||
Get namespace packages (list) but only for
|
|
||||||
single_version_externally_managed installations and empty otherwise.
|
|
||||||
"""
|
|
||||||
# TODO: is it necessary to short-circuit here? i.e. what's the cost
|
|
||||||
# if get_finalized_command is called even when namespace_packages is
|
|
||||||
# False?
|
|
||||||
if not self.distribution.namespace_packages:
|
|
||||||
return []
|
|
||||||
|
|
||||||
install_cmd = self.get_finalized_command('install')
|
|
||||||
svem = install_cmd.single_version_externally_managed
|
|
||||||
|
|
||||||
return self.distribution.namespace_packages if svem else []
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _gen_exclusion_paths():
|
|
||||||
"""
|
|
||||||
Generate file paths to be excluded for namespace packages (bytecode
|
|
||||||
cache files).
|
|
||||||
"""
|
|
||||||
# always exclude the package module itself
|
|
||||||
yield '__init__.py'
|
|
||||||
|
|
||||||
yield '__init__.pyc'
|
|
||||||
yield '__init__.pyo'
|
|
||||||
|
|
||||||
if not hasattr(sys, 'implementation'):
|
|
||||||
return
|
|
||||||
|
|
||||||
base = os.path.join(
|
|
||||||
'__pycache__', '__init__.' + sys.implementation.cache_tag)
|
|
||||||
yield base + '.pyc'
|
|
||||||
yield base + '.pyo'
|
|
||||||
yield base + '.opt-1.pyc'
|
|
||||||
yield base + '.opt-2.pyc'
|
|
||||||
|
|
||||||
def copy_tree(
|
|
||||||
self, infile, outfile,
|
|
||||||
preserve_mode=1, preserve_times=1, preserve_symlinks=0, level=1
|
|
||||||
):
|
|
||||||
assert preserve_mode and preserve_times and not preserve_symlinks
|
|
||||||
exclude = self.get_exclusions()
|
|
||||||
|
|
||||||
if not exclude:
|
|
||||||
import distutils.dir_util
|
|
||||||
distutils.dir_util._multiarch = self.multiarch
|
|
||||||
return orig.install_lib.copy_tree(self, infile, outfile)
|
|
||||||
|
|
||||||
# Exclude namespace package __init__.py* files from the output
|
|
||||||
|
|
||||||
from setuptools.archive_util import unpack_directory
|
|
||||||
from distutils import log
|
|
||||||
|
|
||||||
outfiles = []
|
|
||||||
|
|
||||||
if self.multiarch:
|
|
||||||
import sysconfig
|
|
||||||
ext_suffix = sysconfig.get_config_var ('EXT_SUFFIX')
|
|
||||||
if ext_suffix.endswith(self.multiarch + ext_suffix[-3:]):
|
|
||||||
new_suffix = None
|
|
||||||
else:
|
|
||||||
new_suffix = "%s-%s%s" % (ext_suffix[:-3], self.multiarch, ext_suffix[-3:])
|
|
||||||
|
|
||||||
def pf(src, dst):
|
|
||||||
if dst in exclude:
|
|
||||||
log.warn("Skipping installation of %s (namespace package)",
|
|
||||||
dst)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.multiarch and new_suffix and dst.endswith(ext_suffix) and not dst.endswith(new_suffix):
|
|
||||||
dst = dst.replace(ext_suffix, new_suffix)
|
|
||||||
log.info("renaming extension to %s", os.path.basename(dst))
|
|
||||||
|
|
||||||
log.info("copying %s -> %s", src, os.path.dirname(dst))
|
|
||||||
outfiles.append(dst)
|
|
||||||
return dst
|
|
||||||
|
|
||||||
unpack_directory(infile, outfile, pf)
|
|
||||||
return outfiles
|
|
||||||
|
|
||||||
def get_outputs(self):
|
|
||||||
outputs = orig.install_lib.get_outputs(self)
|
|
||||||
exclude = self.get_exclusions()
|
|
||||||
if exclude:
|
|
||||||
return [f for f in outputs if f not in exclude]
|
|
||||||
return outputs
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
from distutils import log
|
|
||||||
import distutils.command.install_scripts as orig
|
|
||||||
from distutils.errors import DistutilsModuleError
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pkg_resources import Distribution, PathMetadata
|
|
||||||
from .._path import ensure_directory
|
|
||||||
|
|
||||||
|
|
||||||
class install_scripts(orig.install_scripts):
|
|
||||||
"""Do normal script install, plus any egg_info wrapper scripts"""
|
|
||||||
|
|
||||||
def initialize_options(self):
|
|
||||||
orig.install_scripts.initialize_options(self)
|
|
||||||
self.no_ep = False
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import setuptools.command.easy_install as ei
|
|
||||||
|
|
||||||
self.run_command("egg_info")
|
|
||||||
if self.distribution.scripts:
|
|
||||||
orig.install_scripts.run(self) # run first to set up self.outfiles
|
|
||||||
else:
|
|
||||||
self.outfiles = []
|
|
||||||
if self.no_ep:
|
|
||||||
# don't install entry point scripts into .egg file!
|
|
||||||
return
|
|
||||||
|
|
||||||
ei_cmd = self.get_finalized_command("egg_info")
|
|
||||||
dist = Distribution(
|
|
||||||
ei_cmd.egg_base, PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),
|
|
||||||
ei_cmd.egg_name, ei_cmd.egg_version,
|
|
||||||
)
|
|
||||||
bs_cmd = self.get_finalized_command('build_scripts')
|
|
||||||
exec_param = getattr(bs_cmd, 'executable', None)
|
|
||||||
try:
|
|
||||||
bw_cmd = self.get_finalized_command("bdist_wininst")
|
|
||||||
is_wininst = getattr(bw_cmd, '_is_running', False)
|
|
||||||
except (ImportError, DistutilsModuleError):
|
|
||||||
is_wininst = False
|
|
||||||
writer = ei.ScriptWriter
|
|
||||||
if is_wininst:
|
|
||||||
exec_param = "python.exe"
|
|
||||||
writer = ei.WindowsScriptWriter
|
|
||||||
if exec_param == sys.executable:
|
|
||||||
# In case the path to the Python executable contains a space, wrap
|
|
||||||
# it so it's not split up.
|
|
||||||
exec_param = [exec_param]
|
|
||||||
# resolve the writer to the environment
|
|
||||||
writer = writer.best()
|
|
||||||
cmd = writer.command_spec_class.best().from_param(exec_param)
|
|
||||||
for args in writer.get_args(dist, cmd.as_header()):
|
|
||||||
self.write_script(*args)
|
|
||||||
|
|
||||||
def write_script(self, script_name, contents, mode="t", *ignored):
|
|
||||||
"""Write an executable file to the scripts directory"""
|
|
||||||
from setuptools.command.easy_install import chmod, current_umask
|
|
||||||
|
|
||||||
log.info("Installing %s script to %s", script_name, self.install_dir)
|
|
||||||
target = os.path.join(self.install_dir, script_name)
|
|
||||||
self.outfiles.append(target)
|
|
||||||
|
|
||||||
mask = current_umask()
|
|
||||||
if not self.dry_run:
|
|
||||||
ensure_directory(target)
|
|
||||||
f = open(target, "w" + mode)
|
|
||||||
f.write(contents)
|
|
||||||
f.close()
|
|
||||||
chmod(target, 0o777 - mask)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user