This commit is contained in:
2025-01-26 19:24:23 -08:00
parent 32cd60e92b
commit d1dde0dbc6
4155 changed files with 29170 additions and 216373 deletions

View File

@@ -2,12 +2,16 @@
Requirements file parsing
"""
import codecs
import locale
import logging
import optparse
import os
import re
import shlex
import sys
import urllib.parse
from dataclasses import dataclass
from optparse import Values
from typing import (
TYPE_CHECKING,
@@ -25,7 +29,6 @@ from typing import (
from pip._internal.cli import cmdoptions
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
from pip._internal.models.search_scope import SearchScope
from pip._internal.utils.encoding import auto_decode
if TYPE_CHECKING:
from pip._internal.index.package_finder import PackageFinder
@@ -81,52 +84,66 @@ SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [
str(o().dest) for o in SUPPORTED_OPTIONS_EDITABLE_REQ
]
# order of BOMS is important: codecs.BOM_UTF16_LE is a prefix of codecs.BOM_UTF32_LE
# so data.startswith(BOM_UTF16_LE) would be true for UTF32_LE data
BOMS: List[Tuple[bytes, str]] = [
(codecs.BOM_UTF8, "utf-8"),
(codecs.BOM_UTF32, "utf-32"),
(codecs.BOM_UTF32_BE, "utf-32-be"),
(codecs.BOM_UTF32_LE, "utf-32-le"),
(codecs.BOM_UTF16, "utf-16"),
(codecs.BOM_UTF16_BE, "utf-16-be"),
(codecs.BOM_UTF16_LE, "utf-16-le"),
]
PEP263_ENCODING_RE = re.compile(rb"coding[:=]\s*([-\w.]+)")
DEFAULT_ENCODING = "utf-8"
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class ParsedRequirement:
def __init__(
self,
requirement: str,
is_editable: bool,
comes_from: str,
constraint: bool,
options: Optional[Dict[str, Any]] = None,
line_source: Optional[str] = None,
) -> None:
self.requirement = requirement
self.is_editable = is_editable
self.comes_from = comes_from
self.options = options
self.constraint = constraint
self.line_source = line_source
# TODO: replace this with slots=True when dropping Python 3.9 support.
__slots__ = (
"requirement",
"is_editable",
"comes_from",
"constraint",
"options",
"line_source",
)
requirement: str
is_editable: bool
comes_from: str
constraint: bool
options: Optional[Dict[str, Any]]
line_source: Optional[str]
@dataclass(frozen=True)
class ParsedLine:
def __init__(
self,
filename: str,
lineno: int,
args: str,
opts: Values,
constraint: bool,
) -> None:
self.filename = filename
self.lineno = lineno
self.opts = opts
self.constraint = constraint
__slots__ = ("filename", "lineno", "args", "opts", "constraint")
if args:
self.is_requirement = True
self.is_editable = False
self.requirement = args
elif opts.editables:
self.is_requirement = True
self.is_editable = True
filename: str
lineno: int
args: str
opts: Values
constraint: bool
@property
def is_editable(self) -> bool:
return bool(self.opts.editables)
@property
def requirement(self) -> Optional[str]:
if self.args:
return self.args
elif self.is_editable:
# We don't support multiple -e on one line
self.requirement = opts.editables[0]
else:
self.is_requirement = False
return self.opts.editables[0]
return None
def parse_requirements(
@@ -179,7 +196,7 @@ def handle_requirement_line(
line.lineno,
)
assert line.is_requirement
assert line.requirement is not None
# get the options that apply to requirements
if line.is_editable:
@@ -301,7 +318,7 @@ def handle_line(
affect the finder.
"""
if line.is_requirement:
if line.requirement is not None:
parsed_req = handle_requirement_line(line, options)
return parsed_req
else:
@@ -340,7 +357,7 @@ class RequirementsFileParser:
parsed_files_stack: List[Dict[str, Optional[str]]],
) -> Generator[ParsedLine, None, None]:
for line in self._parse_file(filename, constraint):
if not line.is_requirement and (
if line.requirement is None and (
line.opts.requirements or line.opts.constraints
):
# parse a nested requirements file
@@ -568,7 +585,39 @@ def get_file_content(url: str, session: "PipSession") -> Tuple[str, str]:
# Assume this is a bare path.
try:
with open(url, "rb") as f:
content = auto_decode(f.read())
raw_content = f.read()
except OSError as exc:
raise InstallationError(f"Could not open requirements file: {exc}")
content = _decode_req_file(raw_content, url)
return url, content
def _decode_req_file(data: bytes, url: str) -> str:
for bom, encoding in BOMS:
if data.startswith(bom):
return data[len(bom) :].decode(encoding)
for line in data.split(b"\n")[:2]:
if line[0:1] == b"#":
result = PEP263_ENCODING_RE.search(line)
if result is not None:
encoding = result.groups()[0].decode("ascii")
return data.decode(encoding)
try:
return data.decode(DEFAULT_ENCODING)
except UnicodeDecodeError:
locale_encoding = locale.getpreferredencoding(False) or sys.getdefaultencoding()
logging.warning(
"unable to decode data from %s with default encoding %s, "
"falling back to encoding from locale: %s. "
"If this is intentional you should specify the encoding with a "
"PEP-263 style comment, e.g. '# -*- coding: %s -*-'",
url,
DEFAULT_ENCODING,
locale_encoding,
locale_encoding,
)
return data.decode(locale_encoding)

View File

@@ -837,7 +837,7 @@ class InstallRequirement:
"try using --config-settings editable_mode=compat. "
"Please consult the setuptools documentation for more information"
),
gone_in="25.0",
gone_in="25.1",
issue=11457,
)
if self.config_settings:
@@ -925,7 +925,7 @@ def check_legacy_setup_py_options(
reason="--build-option and --global-option are deprecated.",
issue=11859,
replacement="to use --config-settings",
gone_in="25.0",
gone_in=None,
)
logger.warning(
"Implying --no-binary=:all: due to the presence of "