Загрузить файлы в «venv/Lib/site-packages/pip/_internal/req»
This commit is contained in:
103
venv/Lib/site-packages/pip/_internal/req/__init__.py
Normal file
103
venv/Lib/site-packages/pip/_internal/req/__init__.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pip._internal.cli.progress_bars import BarType, get_install_progress_renderer
|
||||
from pip._internal.utils.logging import indent_log
|
||||
|
||||
from .req_file import parse_requirements
|
||||
from .req_install import InstallRequirement
|
||||
from .req_set import RequirementSet
|
||||
|
||||
__all__ = [
|
||||
"RequirementSet",
|
||||
"InstallRequirement",
|
||||
"parse_requirements",
|
||||
"install_given_reqs",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallationResult:
|
||||
name: str
|
||||
|
||||
|
||||
def _validate_requirements(
|
||||
requirements: list[InstallRequirement],
|
||||
) -> Generator[tuple[str, InstallRequirement], None, None]:
|
||||
for req in requirements:
|
||||
assert req.name, f"invalid to-be-installed requirement: {req}"
|
||||
yield req.name, req
|
||||
|
||||
|
||||
def install_given_reqs(
|
||||
requirements: list[InstallRequirement],
|
||||
root: str | None,
|
||||
home: str | None,
|
||||
prefix: str | None,
|
||||
warn_script_location: bool,
|
||||
use_user_site: bool,
|
||||
pycompile: bool,
|
||||
progress_bar: BarType,
|
||||
) -> list[InstallationResult]:
|
||||
"""
|
||||
Install everything in the given list.
|
||||
|
||||
(to be called after having downloaded and unpacked the packages)
|
||||
"""
|
||||
to_install = collections.OrderedDict(_validate_requirements(requirements))
|
||||
|
||||
if to_install:
|
||||
logger.info(
|
||||
"Installing collected packages: %s",
|
||||
", ".join(to_install.keys()),
|
||||
)
|
||||
|
||||
installed = []
|
||||
|
||||
show_progress = logger.isEnabledFor(logging.INFO) and len(to_install) > 1
|
||||
|
||||
items = iter(to_install.values())
|
||||
if show_progress:
|
||||
renderer = get_install_progress_renderer(
|
||||
bar_type=progress_bar, total=len(to_install)
|
||||
)
|
||||
items = renderer(items)
|
||||
|
||||
with indent_log():
|
||||
for requirement in items:
|
||||
req_name = requirement.name
|
||||
assert req_name is not None
|
||||
if requirement.should_reinstall:
|
||||
logger.info("Attempting uninstall: %s", req_name)
|
||||
with indent_log():
|
||||
uninstalled_pathset = requirement.uninstall(auto_confirm=True)
|
||||
else:
|
||||
uninstalled_pathset = None
|
||||
|
||||
try:
|
||||
requirement.install(
|
||||
root=root,
|
||||
home=home,
|
||||
prefix=prefix,
|
||||
warn_script_location=warn_script_location,
|
||||
use_user_site=use_user_site,
|
||||
pycompile=pycompile,
|
||||
)
|
||||
except Exception:
|
||||
# if install did not succeed, rollback previous uninstall
|
||||
if uninstalled_pathset and not requirement.install_succeeded:
|
||||
uninstalled_pathset.rollback()
|
||||
raise
|
||||
else:
|
||||
if uninstalled_pathset and requirement.install_succeeded:
|
||||
uninstalled_pathset.commit()
|
||||
|
||||
installed.append(InstallationResult(req_name))
|
||||
|
||||
return installed
|
||||
568
venv/Lib/site-packages/pip/_internal/req/constructors.py
Normal file
568
venv/Lib/site-packages/pip/_internal/req/constructors.py
Normal file
@@ -0,0 +1,568 @@
|
||||
"""Backing implementation for InstallRequirement's various constructors
|
||||
|
||||
The idea here is that these formed a major chunk of InstallRequirement's size
|
||||
so, moving them and support code dedicated to them outside of that class
|
||||
helps creates for better understandability for the rest of the code.
|
||||
|
||||
These are meant to be used elsewhere within pip to create instances of
|
||||
InstallRequirement.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Collection
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pip._vendor.packaging.markers import Marker
|
||||
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
|
||||
from pip._vendor.packaging.specifiers import Specifier
|
||||
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.models.index import PyPI, TestPyPI
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.req.req_file import ParsedRequirement
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.filetypes import is_archive_file
|
||||
from pip._internal.utils.misc import is_installable_dir
|
||||
from pip._internal.utils.packaging import get_requirement
|
||||
from pip._internal.utils.urls import path_to_url
|
||||
from pip._internal.vcs import is_url, vcs
|
||||
|
||||
__all__ = [
|
||||
"install_req_from_editable",
|
||||
"install_req_from_line",
|
||||
"parse_editable",
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
operators = Specifier._operators.keys()
|
||||
|
||||
|
||||
def _strip_extras(path: str) -> tuple[str, str | None]:
|
||||
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
|
||||
extras = None
|
||||
if m:
|
||||
path_no_extras = m.group(1).rstrip()
|
||||
extras = m.group(2)
|
||||
else:
|
||||
path_no_extras = path
|
||||
|
||||
return path_no_extras, extras
|
||||
|
||||
|
||||
def convert_extras(extras: str | None) -> set[str]:
|
||||
if not extras:
|
||||
return set()
|
||||
return get_requirement("placeholder" + extras.lower()).extras
|
||||
|
||||
|
||||
def _set_requirement_extras(req: Requirement, new_extras: set[str]) -> Requirement:
|
||||
"""
|
||||
Returns a new requirement based on the given one, with the supplied extras. If the
|
||||
given requirement already has extras those are replaced (or dropped if no new extras
|
||||
are given).
|
||||
"""
|
||||
match: re.Match[str] | None = re.fullmatch(
|
||||
# see https://peps.python.org/pep-0508/#complete-grammar
|
||||
r"([\w\t .-]+)(\[[^\]]*\])?(.*)",
|
||||
str(req),
|
||||
flags=re.ASCII,
|
||||
)
|
||||
# ireq.req is a valid requirement so the regex should always match
|
||||
assert (
|
||||
match is not None
|
||||
), f"regex match on requirement {req} failed, this should never happen"
|
||||
pre: str | None = match.group(1)
|
||||
post: str | None = match.group(3)
|
||||
assert (
|
||||
pre is not None and post is not None
|
||||
), f"regex group selection for requirement {req} failed, this should never happen"
|
||||
extras: str = "[{}]".format(",".join(sorted(new_extras)) if new_extras else "")
|
||||
return get_requirement(f"{pre}{extras}{post}")
|
||||
|
||||
|
||||
def _parse_direct_url_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
|
||||
try:
|
||||
req = Requirement(editable_req)
|
||||
except InvalidRequirement:
|
||||
pass
|
||||
else:
|
||||
if req.url:
|
||||
# Join the marker back into the name part. This will be parsed out
|
||||
# later into a Requirement again.
|
||||
if req.marker:
|
||||
name = f"{req.name} ; {req.marker}"
|
||||
else:
|
||||
name = req.name
|
||||
return (name, req.url, req.extras)
|
||||
|
||||
raise ValueError
|
||||
|
||||
|
||||
def _parse_pip_syntax_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
|
||||
url = editable_req
|
||||
|
||||
# If a file path is specified with extras, strip off the extras.
|
||||
url_no_extras, extras = _strip_extras(url)
|
||||
|
||||
if os.path.isdir(url_no_extras):
|
||||
# Treating it as code that has already been checked out
|
||||
url_no_extras = path_to_url(url_no_extras)
|
||||
|
||||
if url_no_extras.lower().startswith("file:"):
|
||||
package_name = Link(url_no_extras).egg_fragment
|
||||
if extras:
|
||||
return (
|
||||
package_name,
|
||||
url_no_extras,
|
||||
get_requirement("placeholder" + extras.lower()).extras,
|
||||
)
|
||||
else:
|
||||
return package_name, url_no_extras, set()
|
||||
|
||||
for version_control in vcs:
|
||||
if url.lower().startswith(f"{version_control}:"):
|
||||
url = f"{version_control}+{url}"
|
||||
break
|
||||
|
||||
return Link(url).egg_fragment, url, set()
|
||||
|
||||
|
||||
def parse_editable(editable_req: str) -> tuple[str | None, str, set[str]]:
|
||||
"""Parses an editable requirement into:
|
||||
- a requirement name with environment markers
|
||||
- an URL
|
||||
- extras
|
||||
Accepted requirements:
|
||||
- svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
|
||||
- local_path[some_extra]
|
||||
- Foobar[extra] @ svn+http://blahblah@rev#subdirectory=subdir ; markers
|
||||
"""
|
||||
try:
|
||||
package_name, url, extras = _parse_direct_url_editable(editable_req)
|
||||
except ValueError:
|
||||
package_name, url, extras = _parse_pip_syntax_editable(editable_req)
|
||||
|
||||
link = Link(url)
|
||||
|
||||
if not link.is_vcs and not link.url.startswith("file:"):
|
||||
backends = ", ".join(vcs.all_schemes)
|
||||
raise InstallationError(
|
||||
f"{editable_req} is not a valid editable requirement. "
|
||||
f"It should either be a path to a local project or a VCS URL "
|
||||
f"(beginning with {backends})."
|
||||
)
|
||||
|
||||
# The project name can be inferred from local file URIs easily.
|
||||
if not package_name and not link.url.startswith("file:"):
|
||||
raise InstallationError(
|
||||
f"Could not detect requirement name for '{editable_req}', "
|
||||
"please specify one with your_package_name @ URL"
|
||||
)
|
||||
return package_name, url, extras
|
||||
|
||||
|
||||
def check_first_requirement_in_file(filename: str) -> None:
|
||||
"""Check if file is parsable as a requirements file.
|
||||
|
||||
This is heavily based on ``pkg_resources.parse_requirements``, but
|
||||
simplified to just check the first meaningful line.
|
||||
|
||||
:raises InvalidRequirement: If the first meaningful line cannot be parsed
|
||||
as an requirement.
|
||||
"""
|
||||
with open(filename, encoding="utf-8", errors="ignore") as f:
|
||||
# Create a steppable iterator, so we can handle \-continuations.
|
||||
lines = (
|
||||
line
|
||||
for line in (line.strip() for line in f)
|
||||
if line and not line.startswith("#") # Skip blank lines/comments.
|
||||
)
|
||||
|
||||
for line in lines:
|
||||
# Drop comments -- a hash without a space may be in a URL.
|
||||
if " #" in line:
|
||||
line = line[: line.find(" #")]
|
||||
# If there is a line continuation, drop it, and append the next line.
|
||||
if line.endswith("\\"):
|
||||
line = line[:-2].strip() + next(lines, "")
|
||||
get_requirement(line)
|
||||
return
|
||||
|
||||
|
||||
def deduce_helpful_msg(req: str) -> str:
|
||||
"""Returns helpful msg in case requirements file does not exist,
|
||||
or cannot be parsed.
|
||||
|
||||
:params req: Requirements file path
|
||||
"""
|
||||
if not os.path.exists(req):
|
||||
return f" File '{req}' does not exist."
|
||||
msg = " The path does exist. "
|
||||
# Try to parse and check if it is a requirements file.
|
||||
try:
|
||||
check_first_requirement_in_file(req)
|
||||
except InvalidRequirement:
|
||||
logger.debug("Cannot parse '%s' as requirements file", req)
|
||||
else:
|
||||
msg += (
|
||||
f"The argument you provided "
|
||||
f"({req}) appears to be a"
|
||||
f" requirements file. If that is the"
|
||||
f" case, use the '-r' flag to install"
|
||||
f" the packages specified within it."
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RequirementParts:
|
||||
requirement: Requirement | None
|
||||
link: Link | None
|
||||
markers: Marker | None
|
||||
extras: set[str]
|
||||
|
||||
|
||||
def parse_req_from_editable(editable_req: str) -> RequirementParts:
|
||||
name, url, extras_override = parse_editable(editable_req)
|
||||
|
||||
if name is not None:
|
||||
try:
|
||||
req: Requirement | None = get_requirement(name)
|
||||
except InvalidRequirement as exc:
|
||||
raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
|
||||
else:
|
||||
req = None
|
||||
|
||||
link = Link(url)
|
||||
|
||||
return RequirementParts(req, link, None, extras_override)
|
||||
|
||||
|
||||
# ---- The actual constructors follow ----
|
||||
|
||||
|
||||
def install_req_from_editable(
|
||||
editable_req: str,
|
||||
comes_from: InstallRequirement | str | None = None,
|
||||
*,
|
||||
isolated: bool = False,
|
||||
hash_options: dict[str, list[str]] | None = None,
|
||||
constraint: bool = False,
|
||||
user_supplied: bool = False,
|
||||
permit_editable_wheels: bool = False,
|
||||
config_settings: dict[str, str | list[str]] | None = None,
|
||||
) -> InstallRequirement:
|
||||
if constraint:
|
||||
raise InstallationError("Editable requirements are not allowed as constraints")
|
||||
|
||||
parts = parse_req_from_editable(editable_req)
|
||||
return InstallRequirement(
|
||||
parts.requirement,
|
||||
comes_from=comes_from,
|
||||
user_supplied=user_supplied,
|
||||
editable=True,
|
||||
permit_editable_wheels=permit_editable_wheels,
|
||||
link=parts.link,
|
||||
constraint=constraint,
|
||||
isolated=isolated,
|
||||
hash_options=hash_options,
|
||||
config_settings=config_settings,
|
||||
extras=parts.extras,
|
||||
)
|
||||
|
||||
|
||||
def _looks_like_path(name: str) -> bool:
|
||||
"""Checks whether the string "looks like" a path on the filesystem.
|
||||
|
||||
This does not check whether the target actually exists, only judge from the
|
||||
appearance.
|
||||
|
||||
Returns true if any of the following conditions is true:
|
||||
* a path separator is found (either os.path.sep or os.path.altsep);
|
||||
* a dot is found (which represents the current directory).
|
||||
"""
|
||||
if os.path.sep in name:
|
||||
return True
|
||||
if os.path.altsep is not None and os.path.altsep in name:
|
||||
return True
|
||||
if name.startswith("."):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_url_from_path(path: str, name: str) -> str | None:
|
||||
"""
|
||||
First, it checks whether a provided path is an installable directory. If it
|
||||
is, returns the path.
|
||||
|
||||
If false, check if the path is an archive file (such as a .whl).
|
||||
The function checks if the path is a file. If false, if the path has
|
||||
an @, it will treat it as a PEP 440 URL requirement and return the path.
|
||||
"""
|
||||
if _looks_like_path(name) and os.path.isdir(path):
|
||||
if is_installable_dir(path):
|
||||
return path_to_url(path)
|
||||
# TODO: The is_installable_dir test here might not be necessary
|
||||
# now that it is done in load_pyproject_toml too.
|
||||
raise InstallationError(
|
||||
f"Directory {name!r} is not installable. Neither 'setup.py' "
|
||||
"nor 'pyproject.toml' found."
|
||||
)
|
||||
if not is_archive_file(path):
|
||||
return None
|
||||
if os.path.isfile(path):
|
||||
return path_to_url(path)
|
||||
urlreq_parts = name.split("@", 1)
|
||||
if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
|
||||
# If the path contains '@' and the part before it does not look
|
||||
# like a path, try to treat it as a PEP 440 URL req instead.
|
||||
return None
|
||||
logger.warning(
|
||||
"Requirement %r looks like a filename, but the file does not exist",
|
||||
name,
|
||||
)
|
||||
return path_to_url(path)
|
||||
|
||||
|
||||
def parse_req_from_line(name: str, line_source: str | None) -> RequirementParts:
|
||||
if is_url(name):
|
||||
marker_sep = "; "
|
||||
else:
|
||||
marker_sep = ";"
|
||||
if marker_sep in name:
|
||||
name, markers_as_string = name.split(marker_sep, 1)
|
||||
markers_as_string = markers_as_string.strip()
|
||||
if not markers_as_string:
|
||||
markers = None
|
||||
else:
|
||||
markers = Marker(markers_as_string)
|
||||
else:
|
||||
markers = None
|
||||
name = name.strip()
|
||||
req_as_string = None
|
||||
path = os.path.normpath(os.path.abspath(name))
|
||||
link = None
|
||||
extras_as_string = None
|
||||
|
||||
if is_url(name):
|
||||
link = Link(name)
|
||||
else:
|
||||
p, extras_as_string = _strip_extras(path)
|
||||
url = _get_url_from_path(p, name)
|
||||
if url is not None:
|
||||
link = Link(url)
|
||||
|
||||
# it's a local file, dir, or url
|
||||
if link:
|
||||
# Handle relative file URLs
|
||||
if link.scheme == "file" and re.search(r"\.\./", link.url):
|
||||
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
|
||||
# wheel file
|
||||
if link.is_wheel:
|
||||
wheel = Wheel(link.filename) # can raise InvalidWheelFilename
|
||||
req_as_string = f"{wheel.name}=={wheel.version}"
|
||||
else:
|
||||
# set the req to the egg fragment. when it's not there, this
|
||||
# will become an 'unnamed' requirement
|
||||
req_as_string = link.egg_fragment
|
||||
|
||||
# a requirement specifier
|
||||
else:
|
||||
req_as_string = name
|
||||
|
||||
extras = convert_extras(extras_as_string)
|
||||
|
||||
def with_source(text: str) -> str:
|
||||
if not line_source:
|
||||
return text
|
||||
return f"{text} (from {line_source})"
|
||||
|
||||
def _parse_req_string(req_as_string: str) -> Requirement:
|
||||
try:
|
||||
return get_requirement(req_as_string)
|
||||
except InvalidRequirement as exc:
|
||||
if os.path.sep in req_as_string:
|
||||
add_msg = "It looks like a path."
|
||||
add_msg += deduce_helpful_msg(req_as_string)
|
||||
elif "=" in req_as_string and not any(
|
||||
op in req_as_string for op in operators
|
||||
):
|
||||
add_msg = "= is not a valid operator. Did you mean == ?"
|
||||
else:
|
||||
add_msg = ""
|
||||
msg = with_source(f"Invalid requirement: {req_as_string!r}: {exc}")
|
||||
if add_msg:
|
||||
msg += f"\nHint: {add_msg}"
|
||||
raise InstallationError(msg)
|
||||
|
||||
if req_as_string is not None:
|
||||
req: Requirement | None = _parse_req_string(req_as_string)
|
||||
else:
|
||||
req = None
|
||||
|
||||
return RequirementParts(req, link, markers, extras)
|
||||
|
||||
|
||||
def install_req_from_line(
|
||||
name: str,
|
||||
comes_from: str | InstallRequirement | None = None,
|
||||
*,
|
||||
isolated: bool = False,
|
||||
hash_options: dict[str, list[str]] | None = None,
|
||||
constraint: bool = False,
|
||||
line_source: str | None = None,
|
||||
user_supplied: bool = False,
|
||||
config_settings: dict[str, str | list[str]] | None = None,
|
||||
) -> InstallRequirement:
|
||||
"""Creates an InstallRequirement from a name, which might be a
|
||||
requirement, directory containing 'setup.py', filename, or URL.
|
||||
|
||||
:param line_source: An optional string describing where the line is from,
|
||||
for logging purposes in case of an error.
|
||||
"""
|
||||
parts = parse_req_from_line(name, line_source)
|
||||
|
||||
return InstallRequirement(
|
||||
parts.requirement,
|
||||
comes_from,
|
||||
link=parts.link,
|
||||
markers=parts.markers,
|
||||
isolated=isolated,
|
||||
hash_options=hash_options,
|
||||
config_settings=config_settings,
|
||||
constraint=constraint,
|
||||
extras=parts.extras,
|
||||
user_supplied=user_supplied,
|
||||
)
|
||||
|
||||
|
||||
def install_req_from_req_string(
|
||||
req_string: str,
|
||||
comes_from: InstallRequirement | None = None,
|
||||
isolated: bool = False,
|
||||
user_supplied: bool = False,
|
||||
) -> InstallRequirement:
|
||||
try:
|
||||
req = get_requirement(req_string)
|
||||
except InvalidRequirement as exc:
|
||||
raise InstallationError(f"Invalid requirement: {req_string!r}: {exc}")
|
||||
|
||||
domains_not_allowed = [
|
||||
PyPI.file_storage_domain,
|
||||
TestPyPI.file_storage_domain,
|
||||
]
|
||||
if (
|
||||
req.url
|
||||
and comes_from
|
||||
and comes_from.link
|
||||
and comes_from.link.netloc in domains_not_allowed
|
||||
):
|
||||
# Explicitly disallow pypi packages that depend on external urls
|
||||
raise InstallationError(
|
||||
"Packages installed from PyPI cannot depend on packages "
|
||||
"which are not also hosted on PyPI.\n"
|
||||
f"{comes_from.name} depends on {req} "
|
||||
)
|
||||
|
||||
return InstallRequirement(
|
||||
req,
|
||||
comes_from,
|
||||
isolated=isolated,
|
||||
user_supplied=user_supplied,
|
||||
)
|
||||
|
||||
|
||||
def install_req_from_parsed_requirement(
|
||||
parsed_req: ParsedRequirement,
|
||||
isolated: bool = False,
|
||||
user_supplied: bool = False,
|
||||
config_settings: dict[str, str | list[str]] | None = None,
|
||||
) -> InstallRequirement:
|
||||
if parsed_req.is_editable:
|
||||
req = install_req_from_editable(
|
||||
parsed_req.requirement,
|
||||
comes_from=parsed_req.comes_from,
|
||||
constraint=parsed_req.constraint,
|
||||
isolated=isolated,
|
||||
user_supplied=user_supplied,
|
||||
config_settings=config_settings,
|
||||
)
|
||||
|
||||
else:
|
||||
req = install_req_from_line(
|
||||
parsed_req.requirement,
|
||||
comes_from=parsed_req.comes_from,
|
||||
isolated=isolated,
|
||||
hash_options=(
|
||||
parsed_req.options.get("hashes", {}) if parsed_req.options else {}
|
||||
),
|
||||
constraint=parsed_req.constraint,
|
||||
line_source=parsed_req.line_source,
|
||||
user_supplied=user_supplied,
|
||||
config_settings=config_settings,
|
||||
)
|
||||
return req
|
||||
|
||||
|
||||
def install_req_from_link_and_ireq(
|
||||
link: Link, ireq: InstallRequirement
|
||||
) -> InstallRequirement:
|
||||
return InstallRequirement(
|
||||
req=ireq.req,
|
||||
comes_from=ireq.comes_from,
|
||||
editable=ireq.editable,
|
||||
link=link,
|
||||
markers=ireq.markers,
|
||||
isolated=ireq.isolated,
|
||||
hash_options=ireq.hash_options,
|
||||
config_settings=ireq.config_settings,
|
||||
user_supplied=ireq.user_supplied,
|
||||
)
|
||||
|
||||
|
||||
def install_req_drop_extras(ireq: InstallRequirement) -> InstallRequirement:
|
||||
"""
|
||||
Creates a new InstallationRequirement using the given template but without
|
||||
any extras. Sets the original requirement as the new one's parent
|
||||
(comes_from).
|
||||
"""
|
||||
return InstallRequirement(
|
||||
req=(
|
||||
_set_requirement_extras(ireq.req, set()) if ireq.req is not None else None
|
||||
),
|
||||
comes_from=ireq,
|
||||
editable=ireq.editable,
|
||||
link=ireq.link,
|
||||
markers=ireq.markers,
|
||||
isolated=ireq.isolated,
|
||||
hash_options=ireq.hash_options,
|
||||
constraint=ireq.constraint,
|
||||
extras=[],
|
||||
config_settings=ireq.config_settings,
|
||||
user_supplied=ireq.user_supplied,
|
||||
permit_editable_wheels=ireq.permit_editable_wheels,
|
||||
)
|
||||
|
||||
|
||||
def install_req_extend_extras(
|
||||
ireq: InstallRequirement,
|
||||
extras: Collection[str],
|
||||
) -> InstallRequirement:
|
||||
"""
|
||||
Returns a copy of an installation requirement with some additional extras.
|
||||
Makes a shallow copy of the ireq object.
|
||||
"""
|
||||
result = copy.copy(ireq)
|
||||
result.extras = {*ireq.extras, *extras}
|
||||
result.req = (
|
||||
_set_requirement_extras(ireq.req, result.extras)
|
||||
if ireq.req is not None
|
||||
else None
|
||||
)
|
||||
return result
|
||||
41
venv/Lib/site-packages/pip/_internal/req/pep723.py
Normal file
41
venv/Lib/site-packages/pip/_internal/req/pep723.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from pip._internal.utils.compat import tomllib
|
||||
|
||||
REGEX = r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$"
|
||||
|
||||
|
||||
class PEP723Exception(ValueError):
|
||||
"""Raised to indicate a problem when parsing PEP 723 metadata from a script"""
|
||||
|
||||
def __init__(self, msg: str) -> None:
|
||||
self.msg = msg
|
||||
|
||||
|
||||
def pep723_metadata(scriptfile: str) -> dict[str, Any]:
|
||||
with open(scriptfile) as f:
|
||||
script = f.read()
|
||||
|
||||
name = "script"
|
||||
matches = list(
|
||||
filter(lambda m: m.group("type") == name, re.finditer(REGEX, script))
|
||||
)
|
||||
|
||||
if len(matches) > 1:
|
||||
raise PEP723Exception(f"Multiple {name!r} blocks found in {scriptfile!r}")
|
||||
elif len(matches) == 1:
|
||||
content = "".join(
|
||||
line[2:] if line.startswith("# ") else line[1:]
|
||||
for line in matches[0].group("content").splitlines(keepends=True)
|
||||
)
|
||||
try:
|
||||
metadata = tomllib.loads(content)
|
||||
except Exception as exc:
|
||||
raise PEP723Exception(f"Failed to parse TOML in {scriptfile!r}") from exc
|
||||
else:
|
||||
raise PEP723Exception(
|
||||
f"File does not contain {name!r} metadata: {scriptfile!r}"
|
||||
)
|
||||
|
||||
return metadata
|
||||
@@ -0,0 +1,75 @@
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import Any
|
||||
|
||||
from pip._vendor.dependency_groups import DependencyGroupResolver
|
||||
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.utils.compat import tomllib
|
||||
|
||||
|
||||
def parse_dependency_groups(groups: list[tuple[str, str]]) -> list[str]:
|
||||
"""
|
||||
Parse dependency groups data as provided via the CLI, in a `[path:]group` syntax.
|
||||
|
||||
Raises InstallationErrors if anything goes wrong.
|
||||
"""
|
||||
resolvers = _build_resolvers(path for (path, _) in groups)
|
||||
return list(_resolve_all_groups(resolvers, groups))
|
||||
|
||||
|
||||
def _resolve_all_groups(
|
||||
resolvers: dict[str, DependencyGroupResolver], groups: list[tuple[str, str]]
|
||||
) -> Iterator[str]:
|
||||
"""
|
||||
Run all resolution, converting any error from `DependencyGroupResolver` into
|
||||
an InstallationError.
|
||||
"""
|
||||
for path, groupname in groups:
|
||||
resolver = resolvers[path]
|
||||
try:
|
||||
yield from (str(req) for req in resolver.resolve(groupname))
|
||||
except (ValueError, TypeError, LookupError) as e:
|
||||
raise InstallationError(
|
||||
f"[dependency-groups] resolution failed for '{groupname}' "
|
||||
f"from '{path}': {e}"
|
||||
) from e
|
||||
|
||||
|
||||
def _build_resolvers(paths: Iterable[str]) -> dict[str, Any]:
|
||||
resolvers = {}
|
||||
for path in paths:
|
||||
if path in resolvers:
|
||||
continue
|
||||
|
||||
pyproject = _load_pyproject(path)
|
||||
if "dependency-groups" not in pyproject:
|
||||
raise InstallationError(
|
||||
f"[dependency-groups] table was missing from '{path}'. "
|
||||
"Cannot resolve '--group' option."
|
||||
)
|
||||
raw_dependency_groups = pyproject["dependency-groups"]
|
||||
if not isinstance(raw_dependency_groups, dict):
|
||||
raise InstallationError(
|
||||
f"[dependency-groups] table was malformed in {path}. "
|
||||
"Cannot resolve '--group' option."
|
||||
)
|
||||
|
||||
resolvers[path] = DependencyGroupResolver(raw_dependency_groups)
|
||||
return resolvers
|
||||
|
||||
|
||||
def _load_pyproject(path: str) -> dict[str, Any]:
|
||||
"""
|
||||
This helper loads a pyproject.toml as TOML.
|
||||
|
||||
It raises an InstallationError if the operation fails.
|
||||
"""
|
||||
try:
|
||||
with open(path, "rb") as fp:
|
||||
return tomllib.load(fp)
|
||||
except FileNotFoundError:
|
||||
raise InstallationError(f"{path} not found. Cannot resolve '--group' option.")
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
raise InstallationError(f"Error parsing {path}: {e}") from e
|
||||
except OSError as e:
|
||||
raise InstallationError(f"Error reading {path}: {e}") from e
|
||||
631
venv/Lib/site-packages/pip/_internal/req/req_file.py
Normal file
631
venv/Lib/site-packages/pip/_internal/req/req_file.py
Normal file
@@ -0,0 +1,631 @@
|
||||
"""
|
||||
Requirements file parsing
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
import locale
|
||||
import logging
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
import urllib.parse
|
||||
from collections.abc import Generator, Iterable
|
||||
from dataclasses import dataclass
|
||||
from optparse import Values
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
NoReturn,
|
||||
)
|
||||
|
||||
from pip._internal.cli import cmdoptions
|
||||
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
|
||||
from pip._internal.models.release_control import ReleaseControl
|
||||
from pip._internal.models.search_scope import SearchScope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pip._internal.index.package_finder import PackageFinder
|
||||
from pip._internal.network.session import PipSession
|
||||
|
||||
__all__ = ["parse_requirements"]
|
||||
|
||||
ReqFileLines = Iterable[tuple[int, str]]
|
||||
|
||||
LineParser = Callable[[str], tuple[str, Values]]
|
||||
|
||||
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
|
||||
COMMENT_RE = re.compile(r"(^|\s+)#.*$")
|
||||
|
||||
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
|
||||
# variable name consisting of only uppercase letters, digits or the '_'
|
||||
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
|
||||
# 2013 Edition.
|
||||
ENV_VAR_RE = re.compile(r"(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})")
|
||||
|
||||
SUPPORTED_OPTIONS: list[Callable[..., optparse.Option]] = [
|
||||
cmdoptions.index_url,
|
||||
cmdoptions.extra_index_url,
|
||||
cmdoptions.no_index,
|
||||
cmdoptions.constraints,
|
||||
cmdoptions.requirements,
|
||||
cmdoptions.editable,
|
||||
cmdoptions.find_links,
|
||||
cmdoptions.no_binary,
|
||||
cmdoptions.only_binary,
|
||||
cmdoptions.prefer_binary,
|
||||
cmdoptions.require_hashes,
|
||||
cmdoptions.pre,
|
||||
cmdoptions.all_releases,
|
||||
cmdoptions.only_final,
|
||||
cmdoptions.trusted_host,
|
||||
cmdoptions.use_new_feature,
|
||||
]
|
||||
|
||||
# options to be passed to requirements
|
||||
SUPPORTED_OPTIONS_REQ: list[Callable[..., optparse.Option]] = [
|
||||
cmdoptions.hash,
|
||||
cmdoptions.config_settings,
|
||||
]
|
||||
|
||||
SUPPORTED_OPTIONS_EDITABLE_REQ: list[Callable[..., optparse.Option]] = [
|
||||
cmdoptions.config_settings,
|
||||
]
|
||||
|
||||
|
||||
# the 'dest' string values
|
||||
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
|
||||
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:
|
||||
# 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: dict[str, Any] | None
|
||||
line_source: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedLine:
|
||||
__slots__ = ("filename", "lineno", "args", "opts", "constraint")
|
||||
|
||||
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) -> str | None:
|
||||
if self.args:
|
||||
return self.args
|
||||
elif self.is_editable:
|
||||
# We don't support multiple -e on one line
|
||||
return self.opts.editables[0]
|
||||
return None
|
||||
|
||||
|
||||
def parse_requirements(
|
||||
filename: str,
|
||||
session: PipSession,
|
||||
finder: PackageFinder | None = None,
|
||||
options: optparse.Values | None = None,
|
||||
constraint: bool = False,
|
||||
) -> Generator[ParsedRequirement, None, None]:
|
||||
"""Parse a requirements file and yield ParsedRequirement instances.
|
||||
|
||||
:param filename: Path or url of requirements file.
|
||||
:param session: PipSession instance.
|
||||
:param finder: Instance of pip.index.PackageFinder.
|
||||
:param options: cli options.
|
||||
:param constraint: If true, parsing a constraint file rather than
|
||||
requirements file.
|
||||
"""
|
||||
line_parser = get_line_parser(finder)
|
||||
parser = RequirementsFileParser(session, line_parser)
|
||||
|
||||
for parsed_line in parser.parse(filename, constraint):
|
||||
parsed_req = handle_line(
|
||||
parsed_line, options=options, finder=finder, session=session
|
||||
)
|
||||
if parsed_req is not None:
|
||||
yield parsed_req
|
||||
|
||||
|
||||
def preprocess(content: str) -> ReqFileLines:
|
||||
"""Split, filter, and join lines, and return a line iterator
|
||||
|
||||
:param content: the content of the requirements file
|
||||
"""
|
||||
lines_enum: ReqFileLines = enumerate(content.splitlines(), start=1)
|
||||
lines_enum = join_lines(lines_enum)
|
||||
lines_enum = ignore_comments(lines_enum)
|
||||
lines_enum = expand_env_variables(lines_enum)
|
||||
return lines_enum
|
||||
|
||||
|
||||
def handle_requirement_line(
|
||||
line: ParsedLine,
|
||||
options: optparse.Values | None = None,
|
||||
) -> ParsedRequirement:
|
||||
# preserve for the nested code path
|
||||
line_comes_from = "{} {} (line {})".format(
|
||||
"-c" if line.constraint else "-r",
|
||||
line.filename,
|
||||
line.lineno,
|
||||
)
|
||||
|
||||
assert line.requirement is not None
|
||||
|
||||
# get the options that apply to requirements
|
||||
if line.is_editable:
|
||||
supported_dest = SUPPORTED_OPTIONS_EDITABLE_REQ_DEST
|
||||
else:
|
||||
supported_dest = SUPPORTED_OPTIONS_REQ_DEST
|
||||
req_options = {}
|
||||
for dest in supported_dest:
|
||||
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
|
||||
req_options[dest] = line.opts.__dict__[dest]
|
||||
|
||||
line_source = f"line {line.lineno} of {line.filename}"
|
||||
return ParsedRequirement(
|
||||
requirement=line.requirement,
|
||||
is_editable=line.is_editable,
|
||||
comes_from=line_comes_from,
|
||||
constraint=line.constraint,
|
||||
options=req_options,
|
||||
line_source=line_source,
|
||||
)
|
||||
|
||||
|
||||
def handle_option_line(
|
||||
opts: Values,
|
||||
filename: str,
|
||||
lineno: int,
|
||||
finder: PackageFinder | None = None,
|
||||
options: optparse.Values | None = None,
|
||||
session: PipSession | None = None,
|
||||
) -> None:
|
||||
if opts.hashes:
|
||||
logger.warning(
|
||||
"%s line %s has --hash but no requirement, and will be ignored.",
|
||||
filename,
|
||||
lineno,
|
||||
)
|
||||
|
||||
if options:
|
||||
# percolate options upward
|
||||
if opts.require_hashes:
|
||||
options.require_hashes = opts.require_hashes
|
||||
if opts.features_enabled:
|
||||
options.features_enabled.extend(
|
||||
f for f in opts.features_enabled if f not in options.features_enabled
|
||||
)
|
||||
|
||||
# set finder options
|
||||
if finder:
|
||||
find_links = finder.find_links
|
||||
index_urls = finder.index_urls
|
||||
no_index = finder.search_scope.no_index
|
||||
if opts.no_index is True:
|
||||
no_index = True
|
||||
index_urls = []
|
||||
if opts.index_url and not no_index:
|
||||
index_urls = [opts.index_url]
|
||||
if opts.extra_index_urls and not no_index:
|
||||
index_urls.extend(opts.extra_index_urls)
|
||||
if opts.find_links:
|
||||
# FIXME: it would be nice to keep track of the source
|
||||
# of the find_links: support a find-links local path
|
||||
# relative to a requirements file.
|
||||
value = opts.find_links[0]
|
||||
req_dir = os.path.dirname(os.path.abspath(filename))
|
||||
relative_to_reqs_file = os.path.join(req_dir, value)
|
||||
if os.path.exists(relative_to_reqs_file):
|
||||
value = relative_to_reqs_file
|
||||
find_links.append(value)
|
||||
|
||||
if session:
|
||||
# We need to update the auth urls in session
|
||||
session.update_index_urls(index_urls)
|
||||
|
||||
search_scope = SearchScope(
|
||||
find_links=find_links,
|
||||
index_urls=index_urls,
|
||||
no_index=no_index,
|
||||
)
|
||||
finder.search_scope = search_scope
|
||||
|
||||
# Transform --pre into --all-releases :all:
|
||||
if opts.pre:
|
||||
if not opts.release_control:
|
||||
opts.release_control = ReleaseControl()
|
||||
opts.release_control.all_releases.add(":all:")
|
||||
|
||||
if opts.release_control:
|
||||
if not finder.release_control:
|
||||
# First time seeing release_control, set it on finder
|
||||
finder.set_release_control(opts.release_control)
|
||||
|
||||
if opts.prefer_binary:
|
||||
finder.set_prefer_binary()
|
||||
|
||||
if session:
|
||||
for host in opts.trusted_hosts or []:
|
||||
source = f"line {lineno} of {filename}"
|
||||
session.add_trusted_host(host, source=source)
|
||||
|
||||
|
||||
def handle_line(
|
||||
line: ParsedLine,
|
||||
options: optparse.Values | None = None,
|
||||
finder: PackageFinder | None = None,
|
||||
session: PipSession | None = None,
|
||||
) -> ParsedRequirement | None:
|
||||
"""Handle a single parsed requirements line; This can result in
|
||||
creating/yielding requirements, or updating the finder.
|
||||
|
||||
:param line: The parsed line to be processed.
|
||||
:param options: CLI options.
|
||||
:param finder: The finder - updated by non-requirement lines.
|
||||
:param session: The session - updated by non-requirement lines.
|
||||
|
||||
Returns a ParsedRequirement object if the line is a requirement line,
|
||||
otherwise returns None.
|
||||
|
||||
For lines that contain requirements, the only options that have an effect
|
||||
are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
|
||||
requirement. Other options from SUPPORTED_OPTIONS may be present, but are
|
||||
ignored.
|
||||
|
||||
For lines that do not contain requirements, the only options that have an
|
||||
effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may
|
||||
be present, but are ignored. These lines may contain multiple options
|
||||
(although our docs imply only one is supported), and all our parsed and
|
||||
affect the finder.
|
||||
"""
|
||||
|
||||
if line.requirement is not None:
|
||||
parsed_req = handle_requirement_line(line, options)
|
||||
return parsed_req
|
||||
else:
|
||||
handle_option_line(
|
||||
line.opts,
|
||||
line.filename,
|
||||
line.lineno,
|
||||
finder,
|
||||
options,
|
||||
session,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class RequirementsFileParser:
|
||||
def __init__(
|
||||
self,
|
||||
session: PipSession,
|
||||
line_parser: LineParser,
|
||||
) -> None:
|
||||
self._session = session
|
||||
self._line_parser = line_parser
|
||||
|
||||
def parse(
|
||||
self, filename: str, constraint: bool
|
||||
) -> Generator[ParsedLine, None, None]:
|
||||
"""Parse a given file, yielding parsed lines."""
|
||||
yield from self._parse_and_recurse(
|
||||
filename, constraint, [{os.path.abspath(filename): None}]
|
||||
)
|
||||
|
||||
def _parse_and_recurse(
|
||||
self,
|
||||
filename: str,
|
||||
constraint: bool,
|
||||
parsed_files_stack: list[dict[str, str | None]],
|
||||
) -> Generator[ParsedLine, None, None]:
|
||||
for line in self._parse_file(filename, constraint):
|
||||
if line.requirement is None and (
|
||||
line.opts.requirements or line.opts.constraints
|
||||
):
|
||||
# parse a nested requirements file
|
||||
if line.opts.requirements:
|
||||
req_path = line.opts.requirements[0]
|
||||
nested_constraint = False
|
||||
else:
|
||||
req_path = line.opts.constraints[0]
|
||||
nested_constraint = True
|
||||
|
||||
# original file is over http
|
||||
if SCHEME_RE.search(filename):
|
||||
# do a url join so relative paths work
|
||||
req_path = urllib.parse.urljoin(filename, req_path)
|
||||
# original file and nested file are paths
|
||||
elif not SCHEME_RE.search(req_path):
|
||||
# do a join so relative paths work
|
||||
# and then abspath so that we can identify recursive references
|
||||
req_path = os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(filename),
|
||||
req_path,
|
||||
)
|
||||
)
|
||||
parsed_files = parsed_files_stack[0]
|
||||
if req_path in parsed_files:
|
||||
initial_file = parsed_files[req_path]
|
||||
tail = (
|
||||
f" and again in {initial_file}"
|
||||
if initial_file is not None
|
||||
else ""
|
||||
)
|
||||
raise RequirementsFileParseError(
|
||||
f"{req_path} recursively references itself in {filename}{tail}"
|
||||
)
|
||||
# Keeping a track where was each file first included in
|
||||
new_parsed_files = parsed_files.copy()
|
||||
new_parsed_files[req_path] = filename
|
||||
yield from self._parse_and_recurse(
|
||||
req_path, nested_constraint, [new_parsed_files, *parsed_files_stack]
|
||||
)
|
||||
else:
|
||||
yield line
|
||||
|
||||
def _parse_file(
|
||||
self, filename: str, constraint: bool
|
||||
) -> Generator[ParsedLine, None, None]:
|
||||
_, content = get_file_content(filename, self._session)
|
||||
|
||||
lines_enum = preprocess(content)
|
||||
|
||||
for line_number, line in lines_enum:
|
||||
try:
|
||||
args_str, opts = self._line_parser(line)
|
||||
except OptionParsingError as e:
|
||||
# add offending line
|
||||
msg = f"Invalid requirement: {line}\n{e.msg}"
|
||||
raise RequirementsFileParseError(msg)
|
||||
|
||||
yield ParsedLine(
|
||||
filename,
|
||||
line_number,
|
||||
args_str,
|
||||
opts,
|
||||
constraint,
|
||||
)
|
||||
|
||||
|
||||
def get_line_parser(finder: PackageFinder | None) -> LineParser:
|
||||
def parse_line(line: str) -> tuple[str, Values]:
|
||||
# Build new parser for each line since it accumulates appendable
|
||||
# options.
|
||||
parser = build_parser()
|
||||
defaults = parser.get_default_values()
|
||||
defaults.index_url = None
|
||||
if finder:
|
||||
defaults.format_control = finder.format_control
|
||||
defaults.release_control = finder.release_control
|
||||
|
||||
args_str, options_str = break_args_options(line)
|
||||
|
||||
try:
|
||||
options = shlex.split(options_str)
|
||||
except ValueError as e:
|
||||
raise OptionParsingError(f"Could not split options: {options_str}") from e
|
||||
|
||||
opts, _ = parser.parse_args(options, defaults)
|
||||
|
||||
return args_str, opts
|
||||
|
||||
return parse_line
|
||||
|
||||
|
||||
def break_args_options(line: str) -> tuple[str, str]:
|
||||
"""Break up the line into an args and options string. We only want to shlex
|
||||
(and then optparse) the options, not the args. args can contain markers
|
||||
which are corrupted by shlex.
|
||||
"""
|
||||
tokens = line.split(" ")
|
||||
args = []
|
||||
options = tokens[:]
|
||||
for token in tokens:
|
||||
if token.startswith(("-", "--")):
|
||||
break
|
||||
else:
|
||||
args.append(token)
|
||||
options.pop(0)
|
||||
return " ".join(args), " ".join(options)
|
||||
|
||||
|
||||
class OptionParsingError(Exception):
|
||||
def __init__(self, msg: str) -> None:
|
||||
self.msg = msg
|
||||
|
||||
|
||||
def build_parser() -> optparse.OptionParser:
|
||||
"""
|
||||
Return a parser for parsing requirement lines
|
||||
"""
|
||||
parser = optparse.OptionParser(add_help_option=False)
|
||||
|
||||
option_factories = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
|
||||
for option_factory in option_factories:
|
||||
option = option_factory()
|
||||
parser.add_option(option)
|
||||
|
||||
# By default optparse sys.exits on parsing errors. We want to wrap
|
||||
# that in our own exception.
|
||||
def parser_exit(self: Any, msg: str) -> NoReturn:
|
||||
raise OptionParsingError(msg)
|
||||
|
||||
# NOTE: mypy disallows assigning to a method
|
||||
# https://github.com/python/mypy/issues/2427
|
||||
parser.exit = parser_exit # type: ignore
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
|
||||
"""Joins a line ending in '\' with the previous line (except when following
|
||||
comments). The joined line takes on the index of the first line.
|
||||
"""
|
||||
primary_line_number = None
|
||||
new_line: list[str] = []
|
||||
for line_number, line in lines_enum:
|
||||
if not line.endswith("\\") or COMMENT_RE.match(line):
|
||||
if COMMENT_RE.match(line):
|
||||
# this ensures comments are always matched later
|
||||
line = " " + line
|
||||
if new_line:
|
||||
new_line.append(line)
|
||||
assert primary_line_number is not None
|
||||
yield primary_line_number, "".join(new_line)
|
||||
new_line = []
|
||||
else:
|
||||
yield line_number, line
|
||||
else:
|
||||
if not new_line:
|
||||
primary_line_number = line_number
|
||||
new_line.append(line.strip("\\"))
|
||||
|
||||
# last line contains \
|
||||
if new_line:
|
||||
assert primary_line_number is not None
|
||||
yield primary_line_number, "".join(new_line)
|
||||
|
||||
# TODO: handle space after '\'.
|
||||
|
||||
|
||||
def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines:
|
||||
"""
|
||||
Strips comments and filter empty lines.
|
||||
"""
|
||||
for line_number, line in lines_enum:
|
||||
line = COMMENT_RE.sub("", line)
|
||||
line = line.strip()
|
||||
if line:
|
||||
yield line_number, line
|
||||
|
||||
|
||||
def expand_env_variables(lines_enum: ReqFileLines) -> ReqFileLines:
|
||||
"""Replace all environment variables that can be retrieved via `os.getenv`.
|
||||
|
||||
The only allowed format for environment variables defined in the
|
||||
requirement file is `${MY_VARIABLE_1}` to ensure two things:
|
||||
|
||||
1. Strings that contain a `$` aren't accidentally (partially) expanded.
|
||||
2. Ensure consistency across platforms for requirement files.
|
||||
|
||||
These points are the result of a discussion on the `github pull
|
||||
request #3514 <https://github.com/pypa/pip/pull/3514>`_.
|
||||
|
||||
Valid characters in variable names follow the `POSIX standard
|
||||
<http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
|
||||
to uppercase letter, digits and the `_` (underscore).
|
||||
"""
|
||||
for line_number, line in lines_enum:
|
||||
for env_var, var_name in ENV_VAR_RE.findall(line):
|
||||
value = os.getenv(var_name)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
line = line.replace(env_var, value)
|
||||
|
||||
yield line_number, line
|
||||
|
||||
|
||||
def get_file_content(url: str, session: PipSession) -> tuple[str, str]:
|
||||
"""Gets the content of a file; it may be a filename, file: URL, or
|
||||
http: URL. Returns (location, content). Content is unicode.
|
||||
Respects # -*- coding: declarations on the retrieved files.
|
||||
|
||||
:param url: File path or url.
|
||||
:param session: PipSession instance.
|
||||
"""
|
||||
scheme = urllib.parse.urlsplit(url).scheme
|
||||
# Pip has special support for file:// URLs (LocalFSAdapter).
|
||||
if scheme in ["http", "https", "file"]:
|
||||
# Delay importing heavy network modules until absolutely necessary.
|
||||
from pip._internal.network.utils import raise_for_status
|
||||
|
||||
resp = session.get(url)
|
||||
raise_for_status(resp)
|
||||
return resp.url, resp.text
|
||||
|
||||
# Assume this is a bare path.
|
||||
try:
|
||||
with open(url, "rb") as f:
|
||||
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)
|
||||
Reference in New Issue
Block a user