stop using requirementslib models (#5793)

* Move away from requirementslib models

* Revise test since PEP-440 does not support wildcard versions but does support equivalent compatible release specifiers.

* simplify and remove dead code

* Ensure the os_name marker is AND with the other markers.

* Move what we still need from requirementslib into the pipenv utils and stop vendoring it.

* Remove requirementslib.

* force upgrade of virtualenv for python 3.12

* remove virtualenv-clone

* Update vcs specifiers documentation; infer name from specific pip line formats where possible.

* Provide helpful text and error for recently removed commands

* Set the right log levels and verbosity to show users the errors generated by pip resolver when supplying -v flag

* Fix the collection of all matching package hashes for non-pypi indexes.  Plus lesson from testing torch which contains local identifiers.
This commit is contained in:
Matt Davis
2023-08-19 16:36:52 -04:00
committed by GitHub
parent be8a084503
commit 6ac1451ec8
78 changed files with 3608 additions and 10320 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] # "3.12-dev" Windows CI hangs indefinitely
os: [MacOS, Ubuntu, Windows]
steps:
+1
View File
@@ -25,6 +25,7 @@ myst-parser = {extras = ["linkify"], version = "*"}
invoke = "==2.0.0"
exceptiongroup = "==1.1.0"
tomli = "*"
pyyaml = "==6.0.1"
[packages]
pytz = "*"
Generated
+383 -374
View File
File diff suppressed because it is too large Load Diff
+34 -10
View File
@@ -103,30 +103,54 @@ All sub-dependencies will get added to the `Pipfile.lock` as well. Sub-dependenc
## VCS Dependencies
VCS dependencies from git and other version control systems using URLs formatted according to the following rule:
VCS dependencies from git and other version control systems using URLs formatted using preferred pip line formats:
<vcs_type>+<scheme>://<location>/<user_or_organization>/<repository>@<branch_or_tag>#egg=<package_name>
<vcs_type>+<scheme>://<location>/<user_or_organization>/<repository>@<branch_or_tag>
The only optional section is the `@<branch_or_tag>` section. When using git over SSH, you may use the shorthand vcs and scheme alias `git+git@<location>:<user_or_organization>/<repository>@<branch_or_tag>#egg=<package_name>`. Note that this is translated to `git+ssh://git@<location>` when parsed.
Extras may be specified using the following format when issuing install command:
<package_name><possible_extras>@ <vcs_type>+<scheme>://<location>/<user_or_organization>/<repository>@<branch_or_tag>
Note: that the #egg fragments should only be used for legacy pip lines which are still required in editable requirements.
$ pipenv install -e git+https://github.com/requests/requests.git@v2.31.0#egg=requests
Note that it is **strongly recommended** that you install any version-controlled dependencies in editable mode, using `pipenv install -e`, in order to ensure that dependency resolution can be performed with an up-to-date copy of the repository each time it is performed, and that it includes all known dependencies.
Below is an example usage which installs the git repository located at `https://github.com/requests/requests.git` from tag `v2.20.1` as package name `requests`:
$ pipenv install -e git+https://github.com/requests/requests.git@v2.20.1#egg=requests
Creating a Pipfile for this project...
Installing -e git+https://github.com/requests/requests.git@v2.20.1#egg=requests...
[...snipped...]
Adding -e git+https://github.com/requests/requests.git@v2.20.1#egg=requests to Pipfile's [packages]...
[...]
Resolving -e git+https://github.com/requests/requests.git@v2.20.1#egg=requests...
Added requests to Pipfile's [packages] ...
Installation Succeeded
Pipfile.lock not found, creating...
Locking [packages] dependencies...
Building requirements...
Resolving dependencies...
Success!
Locking [dev-packages] dependencies...
Updated Pipfile.lock (389441cc656bb774aaa28c7e53a35137aace7499ca01668765d528fa79f8acc8)!
Installing dependencies from Pipfile.lock (f8acc8)...
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
$ cat Pipfile
[packages]
requests = {git = "https://github.com/requests/requests.git", editable = true, ref = "v2.20.1"}
requests = {editable = true, ref = "v2.20.1", git = "git+https://github.com/requests/requests.git"}
$ cat Pipfile.lock
...
"requests": {
"editable": true,
"git": "git+https://github.com/requests/requests.git",
"markers": "python_version >= '3.7'",
"ref": "6cfbe1aedd56f8c2f9ff8b968efe65b22669795b"
},
...
Valid values for `<vcs_type>` include `git`, `bzr`, `svn`, and `hg`. Valid values for `<scheme>` include `http`, `https`, `ssh`, and `file`. In specific cases you also have access to other schemes: `svn` may be combined with `svn` as a scheme, and `bzr` can be combined with `sftp` and `lp`.
You can read more about pip's implementation of VCS support `here <https://pip.pypa.io/en/stable/reference/pip_install/#vcs-support>`__. For more information about other options available when specifying VCS dependencies, please check the `Pipfile spec <https://github.com/pypa/pipfile>`_.
You can read more about pip's implementation of VCS support `here <https://pip.pypa.io/en/stable/reference/pip_install/#vcs-support>`__.
## Specifying Package Categories
+2
View File
@@ -0,0 +1,2 @@
Drop requirementslib for managing pip lines and InstallRequirements, bring remaining requirementslib functionality into pipenv.
Fixes numerous reports about extras installs with vcs and file installs; format pip lines correctly to not generate deprecation warnings.
+18 -2
View File
@@ -3,8 +3,18 @@ import os
import sys
import warnings
# This has to come before imports of pipenv
PIPENV_ROOT = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
PIP_ROOT = os.sep.join([PIPENV_ROOT, "patched", "pip"])
sys.path.insert(0, PIPENV_ROOT)
sys.path.insert(0, PIP_ROOT)
# Load patched pip instead of system pip
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
def _ensure_modules():
# Can be removed when we drop pydantic
spec = importlib.util.spec_from_file_location(
"typing_extensions",
location=os.path.join(
@@ -14,6 +24,14 @@ def _ensure_modules():
typing_extensions = importlib.util.module_from_spec(spec)
sys.modules["typing_extensions"] = typing_extensions
spec.loader.exec_module(typing_extensions)
# Ensure when pip gets invoked it uses our patched version
spec = importlib.util.spec_from_file_location(
"pip",
location=os.path.join(os.path.dirname(__file__), "patched", "pip", "__init__.py"),
)
pip = importlib.util.module_from_spec(spec)
sys.modules["pip"] = pip
spec.loader.exec_module(pip)
_ensure_modules()
@@ -26,8 +44,6 @@ warnings.filterwarnings("ignore", category=DependencyWarning)
warnings.filterwarnings("ignore", category=ResourceWarning)
warnings.filterwarnings("ignore", category=UserWarning)
# Load patched pip instead of system pip
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
if os.name == "nt":
from pipenv.vendor import colorama
+5 -6
View File
@@ -216,7 +216,7 @@ def install(state, **kwargs):
requirementstxt=state.installstate.requirementstxt,
pre=state.installstate.pre,
deploy=state.installstate.deploy,
index_url=state.index,
index=state.index,
packages=state.installstate.packages,
editable_packages=state.installstate.editables,
site_packages=state.site_packages,
@@ -610,7 +610,7 @@ def run_open(state, module, *args, **kwargs):
[
state.project._which("python"),
"-c",
"import {0}; print({0}.__file__)".format(module),
f"import {module}; print({module}.__file__)",
]
)
if c.returncode:
@@ -693,11 +693,10 @@ def scripts(state):
scripts = state.project.parsed_pipfile.get("scripts", {})
first_column_width = max(len(word) for word in ["Command"] + list(scripts))
second_column_width = max(len(word) for word in ["Script"] + list(scripts.values()))
lines = ["{0:<{width}} Script".format("Command", width=first_column_width)]
lines.append("{} {}".format("-" * first_column_width, "-" * second_column_width))
lines = [f"{command:<{first_column_width}} Script" for command in ["Command"]]
lines.append(f"{'-' * first_column_width} {'-' * second_column_width}")
lines.extend(
"{0:<{width}} {1}".format(name, script, width=first_column_width)
for name, script in scripts.items()
f"{name:<{first_column_width}} {script}" for name, script in scripts.items()
)
console.print("\n".join(line for line in lines))
+82
View File
@@ -2,6 +2,7 @@ import os
import re
from pipenv.project import Project
from pipenv.utils import err
from pipenv.utils.internet import is_valid_url
from pipenv.vendor.click import (
BadArgumentUsage,
@@ -483,6 +484,83 @@ def validate_pypi_mirror(ctx, param, value):
return value
# OLD REMOVED COMMANDS THAT WE STILL DISPLAY HELP TEXT FOR #
def skip_lock_option(f):
def callback(ctx, param, value):
if value:
err.print(
"The flag --skip-lock has been functionally removed. "
"Without running the lock resolver it is not possible to manage multiple package indexes. "
"Additionally it bypassed the build consistency guarantees provided by maintaining a lock file.",
style="yellow bold",
)
raise ValueError("The flag --skip-lock flag has been removed.")
return value
return option(
"--skip-lock",
is_flag=True,
default=False,
expose_value=False,
envvar="PIPENV_SKIP_LOCK",
callback=callback,
type=click_types.BOOL,
show_envvar=True,
hidden=True, # This hides the option from the help text.
)(f)
def keep_outdated_option(f):
def callback(ctx, param, value):
state = ctx.ensure_object(State)
state.installstate.keep_outdated = value
if value:
err.print(
"The flag --keep-outdated has been removed. "
"The flag did not respect package resolver results and lead to inconsistent lock files. "
"Consider using the `pipenv upgrade` command to selectively upgrade packages.",
style="yellow bold",
)
raise ValueError("The flag --keep-outdated flag has been removed.")
return value
return option(
"--keep-outdated",
is_flag=True,
default=False,
expose_value=False,
callback=callback,
type=click_types.BOOL,
show_envvar=True,
hidden=True, # This hides the option from the help text.
)(f)
def selective_upgrade_option(f):
def callback(ctx, param, value):
state = ctx.ensure_object(State)
state.installstate.selective_upgrade = value
if value:
err.print(
"The flag --selective-upgrade has been removed. "
"The flag was buggy and lead to inconsistent lock files. "
"Consider using the `pipenv upgrade` command to selectively upgrade packages.",
style="yellow bold",
)
raise ValueError("The flag --selective-upgrade flag has been removed.")
return value
return option(
"--selective-upgrade",
is_flag=True,
default=False,
type=click_types.BOOL,
help="Update specified packages.",
callback=callback,
expose_value=False,
)(f)
def common_options(f):
f = pypi_mirror_option(f)
f = verbose_option(f)
@@ -496,6 +574,7 @@ def install_base_options(f):
f = common_options(f)
f = pre_option(f)
f = extra_pip_args(f)
f = keep_outdated_option(f) # Removed, but still displayed in help text.
return f
@@ -505,6 +584,7 @@ def uninstall_options(f):
f = uninstall_dev_option(f)
f = editable_option(f)
f = package_arg(f)
f = skip_lock_option(f) # Removed, but still displayed in help text.
return f
@@ -530,6 +610,8 @@ def install_options(f):
f = ignore_pipfile_option(f)
f = editable_option(f)
f = package_arg(f)
f = skip_lock_option(f) # Removed, but still display help text.
f = selective_upgrade_option(f) # Removed, but still display help text.
return f
+29 -24
View File
@@ -12,21 +12,23 @@ import typing
from pathlib import Path
from sysconfig import get_paths, get_python_version, get_scheme_names
from urllib.parse import urlparse
from urllib.request import url2pathname
import pipenv
from pipenv.patched.pip._internal.commands.install import InstallCommand
from pipenv.patched.pip._internal.index.package_finder import PackageFinder
from pipenv.patched.pip._internal.req.req_install import InstallRequirement
from pipenv.patched.pip._vendor import pkg_resources
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.utils import console
from pipenv.utils.constants import VCS_LIST
from pipenv.utils.dependencies import as_pipfile
from pipenv.utils.fileutils import normalize_path, temp_path
from pipenv.utils.funktools import chunked, unnest
from pipenv.utils.indexes import prepare_pip_source_args
from pipenv.utils.processes import subprocess_run
from pipenv.utils.shell import make_posix
from pipenv.utils.shell import make_posix, temp_environ
from pipenv.vendor.pythonfinder.utils import is_in_path
from pipenv.vendor.requirementslib.fileutils import normalize_path, temp_path
from pipenv.vendor.requirementslib.utils import temp_environ
try:
# this is only in Python3.8 and later
@@ -200,11 +202,6 @@ class Environment:
.. note:: The implementation of this is borrowed from a combination of pip and
virtualenv and is likely to change at some point in the future.
>>> from pipenv.core import project
>>> from pipenv.environment import Environment
>>> env = Environment(prefix=project.virtualenv_location, is_venv=True, sources=project.sources)
>>> import pprint
>>> pprint.pprint(env.base_paths)
{'PATH': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/bin::/bin:/usr/bin',
'PYTHONPATH': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW/lib/python3.7/site-packages',
'data': '/home/hawk/.virtualenvs/pipenv-MfOPs1lW',
@@ -760,38 +757,46 @@ class Environment:
return any(d for d in self.get_distributions() if d.project_name == pkgname)
def is_satisfied(self, req):
def is_satisfied(self, req: InstallRequirement):
match = next(
iter(
d
for d in self.get_distributions()
if canonicalize_name(d.project_name) == req.normalized_name
if req.name
and canonicalize_name(d.project_name) == canonicalize_name(req.name)
),
None,
)
if match is not None:
if req.editable and req.line_instance.is_local and self.find_egg(match):
requested_path = req.line_instance.path
if req.editable and req.link and req.link.is_file:
requested_path = req.link.file_path
if os.path.exists(requested_path):
local_path = requested_path
else:
parsed_url = urlparse(requested_path)
local_path = url2pathname(parsed_url.path)
local_path = parsed_url.path
return requested_path and os.path.samefile(local_path, match.location)
elif match.has_metadata("direct_url.json"):
direct_url_metadata = json.loads(match.get_metadata("direct_url.json"))
commit_id = direct_url_metadata.get("vcs_info", {}).get("commit_id", "")
vcs_type = direct_url_metadata.get("vcs_info", {}).get("vcs", "")
_, pipfile_part = req.as_pipfile().popitem()
return (
vcs_type == req.vcs
and commit_id == req.commit_hash
and direct_url_metadata["url"] == pipfile_part[req.vcs]
requested_revision = direct_url_metadata.get("vcs_info", {}).get(
"requested_revision", ""
)
elif req.is_vcs or req.is_file_or_url:
vcs_type = direct_url_metadata.get("vcs_info", {}).get("vcs", "")
_, pipfile_part = as_pipfile(req).popitem()
vcs_ref = ""
for vcs in VCS_LIST:
if pipfile_part.get(vcs):
vcs_ref = pipfile_part[vcs].rsplit("@", 1)[-1]
break
return (
vcs_type == req.link.scheme
and vcs_ref == requested_revision
and direct_url_metadata["url"] == pipfile_part[req.link.scheme]
)
elif req.link and req.link.is_vcs:
return False
elif req.line_instance.specifiers is not None:
return req.line_instance.specifiers.contains(
elif req.specifier is not None:
return SpecifierSet(str(req.specifier)).contains(
match.version, prereleases=True
)
return True
+1 -1
View File
@@ -5,8 +5,8 @@ import re
import sys
from pipenv.patched.pip._vendor.platformdirs import user_cache_dir
from pipenv.utils.fileutils import normalize_drive
from pipenv.utils.shell import env_to_bool, is_env_truthy, isatty
from pipenv.vendor.requirementslib.fileutils import normalize_drive
# HACK: avoid resolver.py uses the wrong byte code files.
# I hope I can remove this one day.
+1 -1
View File
@@ -5,7 +5,7 @@ import sys
def format_full_version(info):
version = "{0.major}.{0.minor}.{0.micro}".format(info)
version = f"{info.major}.{info.minor}.{info.micro}"
kind = info.releaselevel
if kind != "final":
version += kind[0] + str(info.serial)
+213 -20
View File
@@ -11,6 +11,10 @@ import sys
import urllib.parse
from json.decoder import JSONDecodeError
from pathlib import Path
from urllib import parse
from urllib.parse import unquote
from pipenv.utils.constants import VCS_LIST
try:
import tomllib as toml
@@ -23,16 +27,35 @@ from pipenv.environments import Setting, is_in_virtualenv, normalize_pipfile_pat
from pipenv.patched.pip._internal.commands.install import InstallCommand
from pipenv.patched.pip._internal.configuration import Configuration
from pipenv.patched.pip._internal.exceptions import ConfigurationError
from pipenv.patched.pip._internal.models.link import Link
from pipenv.patched.pip._internal.req.req_install import InstallRequirement
from pipenv.patched.pip._internal.utils.hashes import FAVORITE_HASH
from pipenv.patched.pip._vendor import pkg_resources
from pipenv.utils import err
from pipenv.utils.constants import is_type_checking
from pipenv.utils.dependencies import (
clean_pkg_version,
determine_package_name,
determine_path_specifier,
determine_vcs_specifier,
expansive_install_req_from_line,
get_canonical_names,
is_editable,
pep423_name,
python_version,
)
from pipenv.utils.internet import get_url_name, is_pypi_url, is_valid_url, proper_case
from pipenv.utils.fileutils import open_file
from pipenv.utils.internet import (
PackageIndexHTMLParser,
get_requests_session,
get_url_name,
is_pypi_url,
is_valid_url,
proper_case,
)
from pipenv.utils.locking import atomic_open_for_write
from pipenv.utils.project import get_default_pyproject_backend
from pipenv.utils.requirements import normalize_name
from pipenv.utils.shell import (
find_requirements,
find_windows_executable,
@@ -45,7 +68,6 @@ from pipenv.utils.shell import (
)
from pipenv.utils.toml import cleanup_toml, convert_toml_outline_tables
from pipenv.vendor import click, plette, tomlkit
from pipenv.vendor.requirementslib.models.utils import get_default_pyproject_backend
try:
# this is only in Python3.8 and later
@@ -132,6 +154,7 @@ class Project:
self._environment = None
self._build_system = {"requires": ["setuptools", "wheel"]}
self.python_version = python_version
self.sessions = {} # pip requests sessions
self.s = Setting()
# Load Pip configuration and get items
self.configuration = Configuration(isolated=False, load_only=None)
@@ -212,6 +235,109 @@ class Project:
else:
return ["packages", "dev-packages"] + list(package_categories)
def get_requests_session_for_source(self, source):
if self.sessions.get(source["name"]):
session = self.sessions[source["name"]]
else:
session = get_requests_session(
self.s.PIPENV_MAX_RETRIES, source.get("verify_ssl", True)
)
self.sessions[source["name"]] = session
return session
@classmethod
def prepend_hash_types(cls, checksums, hash_type):
cleaned_checksums = set()
for checksum in checksums:
if not checksum:
continue
if not checksum.startswith(f"{hash_type}:"):
checksum = f"{hash_type}:{checksum}"
cleaned_checksums.add(checksum)
return sorted(cleaned_checksums)
def get_hash_from_link(self, hash_cache, link):
if link.hash and link.hash_name == FAVORITE_HASH:
return f"{link.hash_name}:{link.hash}"
return hash_cache.get_hash(link)
def get_hashes_from_pypi(self, ireq, source):
pkg_url = f"https://pypi.org/pypi/{ireq.name}/json"
session = self.get_requests_session_for_source(source)
try:
collected_hashes = set()
# Grab the hashes from the new warehouse API.
r = session.get(pkg_url, timeout=10)
api_releases = r.json()["releases"]
cleaned_releases = {}
for api_version, api_info in api_releases.items():
api_version = clean_pkg_version(api_version)
cleaned_releases[api_version] = api_info
version = ""
if ireq.specifier:
spec = next(iter(s for s in ireq.specifier), None)
if spec:
version = spec.version
for release in cleaned_releases[version]:
collected_hashes.add(release["digests"][FAVORITE_HASH])
return self.prepend_hash_types(collected_hashes, FAVORITE_HASH)
except (ValueError, KeyError, ConnectionError):
if self.s.is_verbose():
err.print(
f"[bold][red]Warning[/red][/bold]: Error generating hash for {ireq.name}."
)
return None
def get_hashes_from_remote_index_urls(self, ireq, source):
pkg_url = f"{source['url']}/{ireq.name}/"
session = self.get_requests_session_for_source(source)
try:
collected_hashes = set()
# Grab the hashes from the new warehouse API.
response = session.get(pkg_url, timeout=10)
# Create an instance of the parser
parser = PackageIndexHTMLParser()
# Feed the HTML to the parser
parser.feed(response.text)
# Extract hrefs
hrefs = parser.urls
version = ""
if ireq.specifier:
spec = next(iter(s for s in ireq.specifier), None)
if spec:
version = spec.version
for package_url in hrefs:
if version in parse.unquote(package_url):
url_params = parse.urlparse(package_url).fragment
params_dict = parse.parse_qs(url_params)
if params_dict.get(FAVORITE_HASH):
collected_hashes.add(params_dict[FAVORITE_HASH][0])
else: # Fallback to downloading the file to obtain hash
if source["url"] not in package_url:
package_url = f"{source['url']}{package_url}"
link = Link(package_url)
collected_hashes.add(self.get_file_hash(session, link))
return self.prepend_hash_types(collected_hashes, FAVORITE_HASH)
except (ValueError, KeyError, ConnectionError):
if self.s.is_verbose():
click.echo(
"{}: Error generating hash for {}".format(
click.style("Warning", bold=True, fg="red"), ireq.name
),
err=True,
)
return None
def get_file_hash(self, session, link):
h = hashlib.new(FAVORITE_HASH)
err.print(f"Downloading file {link.filename} to obtain hash...")
with open_file(link.url, session) as fp:
for chunk in iter(lambda: fp.read(8096), b""):
h.update(chunk)
return f"{h.name}:{h.hexdigest()}"
@property
def name(self) -> str:
if self._name is None:
@@ -634,7 +760,7 @@ class Project:
@property
def _pipfile(self):
from .vendor.requirementslib.models.pipfile import Pipfile as ReqLibPipfile
from pipenv.utils.pipfile import Pipfile as ReqLibPipfile
pf = ReqLibPipfile.load(self.pipfile_location)
return pf
@@ -660,7 +786,7 @@ class Project:
return packages
def _get_vcs_packages(self, dev=False):
from pipenv.vendor.requirementslib.utils import is_vcs
from pipenv.utils.requirementslib import is_vcs
section = "dev-packages" if dev else "packages"
packages = {
@@ -745,9 +871,7 @@ class Project:
return source
def get_or_create_lockfile(self, categories, from_pipfile=False):
from pipenv.vendor.requirementslib.models.lockfile import (
Lockfile as Req_Lockfile,
)
from pipenv.utils.locking import Lockfile as Req_Lockfile
if from_pipfile and self.pipfile_exists:
lockfile_dict = {}
@@ -868,6 +992,14 @@ class Project:
source["url"] = os.environ["PIPENV_PYPI_MIRROR"]
return sources
def get_default_index(self):
return self.pipfile_sources()[0]
def get_index_by_name(self, index_name):
for source in self.pipfile_sources():
if source.get("name") == index_name:
return source
@property
def sources(self):
if self.lockfile_exists and hasattr(self.lockfile_content, "keys"):
@@ -937,7 +1069,9 @@ class Project:
def get_package_name_in_pipfile(self, package_name, category):
"""Get the equivalent package name in pipfile"""
section = self.parsed_pipfile.get(category, {})
section = self.parsed_pipfile.get(category)
if section is None:
section = {}
package_name = pep423_name(package_name)
for name in section.keys():
if pep423_name(name) == package_name:
@@ -968,31 +1102,90 @@ class Project:
del parsed[category][pkg_name]
self.write_toml(parsed)
def add_package_to_pipfile(self, package, dev=False, category=None):
from .vendor.requirementslib import Requirement
def generate_package_pipfile_entry(self, package, pip_line, category=None):
# Don't re-capitalize file URLs or VCSs.
if not isinstance(package, InstallRequirement):
package = expansive_install_req_from_line(package.strip())
req_name = determine_package_name(package)
path_specifier = determine_path_specifier(package)
vcs_specifier = determine_vcs_specifier(package)
name = self.get_package_name_in_pipfile(req_name, category=category)
normalized_name = normalize_name(req_name)
extras = package.extras
specifier = "*"
if package.req and package.specifier:
specifier = str(package.specifier)
# Construct package requirement
entry = {}
if extras:
entry["extras"] = list(extras)
if path_specifier:
entry["file"] = unquote(path_specifier)
elif vcs_specifier:
for vcs in VCS_LIST:
if vcs in package.link.scheme:
if pip_line.startswith("-e"):
entry["editable"] = True
pip_line = pip_line.replace("-e ", "")
if "[" in pip_line and "]" in pip_line:
extras_section = pip_line.split("[")[1].split("]")[0]
entry["extras"] = sorted(
[extra.strip() for extra in extras_section.split(",")]
)
if "@ " in pip_line:
vcs_part = pip_line.split("@ ", 1)[1]
else:
vcs_part = pip_line
vcs_parts = vcs_part.rsplit("@", 1)
entry["ref"] = vcs_parts[1].split("#", 1)[0].strip()
entry[vcs] = vcs_parts[0].strip()
break
else:
entry["version"] = specifier
if hasattr(package, "index"):
entry["index"] = package.index
if len(entry) == 1 and "version" in entry:
return name, normalized_name, specifier
else:
return name, normalized_name, entry
def add_package_to_pipfile(self, package, pip_line, dev=False, category=None):
category = category if category else "dev-packages" if dev else "packages"
name, normalized_name, entry = self.generate_package_pipfile_entry(
package, pip_line, category=category
)
return self.add_pipfile_entry_to_pipfile(
name, normalized_name, entry, category=category
)
def add_pipfile_entry_to_pipfile(self, name, normalized_name, entry, category=None):
newly_added = False
# Read and append Pipfile.
p = self.parsed_pipfile
# Don't re-capitalize file URLs or VCSs.
if not isinstance(package, Requirement):
package = Requirement.from_line(package.strip(), parse_setup_info=False)
req_name, converted = package.pipfile_entry
category = category if category else "dev-packages" if dev else "packages"
# Set empty group if it doesn't exist yet.
if category not in p:
p[category] = {}
# Add the package to the group.
name = self.get_package_name_in_pipfile(req_name, category=category)
normalized_name = pep423_name(req_name)
if name and name != normalized_name:
self.remove_package_from_pipfile(name, category=category)
# Add the package to the group.
if normalized_name not in p[category]:
newly_added = True
p[category][normalized_name] = converted
p[category][normalized_name] = entry
# Write Pipfile.
self.write_toml(p)
return newly_added, category
return newly_added, category, normalized_name
def src_name_from_url(self, index_url):
name, _, tld_guess = urllib.parse.urlsplit(index_url).netloc.rpartition(".")
+38 -98
View File
@@ -1,11 +1,8 @@
import importlib.util
import json
import logging
import os
import sys
os.environ["PIP_PYTHON_PATH"] = str(sys.executable)
def _ensure_modules():
spec = importlib.util.spec_from_file_location(
@@ -74,21 +71,9 @@ def which(*args, **kwargs):
def handle_parsed_args(parsed):
if "PIPENV_VERBOSITY" in os.environ:
parsed.verbose = int(os.getenv("PIPENV_VERBOSITY"))
if parsed.debug:
parsed.verbose = max(parsed.verbose, 2)
if parsed.verbose > 1:
logging.getLogger("pip").setLevel(logging.DEBUG)
elif parsed.verbose > 0:
logging.getLogger("pip").setLevel(logging.INFO)
logger = logging.getLogger(
"pipenv.patched.pip._internal.resolution.resolvelib.reporter"
)
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.INFO)
os.environ["PIP_RESOLVER_DEBUG"] = ""
os.environ["PIPENV_VERBOSITY"] = str(parsed.verbose)
if parsed.verbose:
os.environ["PIPENV_VERBOSITY"] = "1"
os.environ["PIP_RESOLVER_DEBUG"] = "1"
if parsed.constraints_file:
with open(parsed.constraints_file) as constraints:
file_constraints = constraints.read().strip().split("\n")
@@ -107,7 +92,7 @@ class Entry:
from pipenv.utils.dependencies import (
get_lockfile_section_using_pipfile_category,
)
from pipenv.vendor.requirementslib.models.utils import tomlkit_value_to_python
from pipenv.utils.toml import tomlkit_value_to_python
self.name = name
if isinstance(entry_dict, dict):
@@ -141,14 +126,19 @@ class Entry:
@staticmethod
def make_requirement(name=None, entry=None):
from pipenv.vendor.requirementslib.models.requirements import Requirement
from pipenv.utils.dependencies import from_pipfile
return Requirement.from_pipfile(name, entry)
return from_pipfile(name, entry)
@classmethod
def clean_initial_dict(cls, entry_dict):
if not entry_dict.get("version", "").startswith("=="):
entry_dict["version"] = cls.clean_specifier(entry_dict.get("version", ""))
from pipenv.patched.pip._vendor.packaging.requirements import Requirement
entry_dict.get("version", "")
version = entry_dict.get("version", "")
if isinstance(version, Requirement):
version = str(version.specifier)
entry_dict["version"] = cls.clean_specifier(version)
if "name" in entry_dict:
del entry_dict["name"]
return entry_dict
@@ -176,7 +166,7 @@ class Entry:
@classmethod
def get_markers_from_dict(cls, entry_dict):
from pipenv.patched.pip._vendor.packaging import markers as packaging_markers
from pipenv.vendor.requirementslib.models.markers import normalize_marker_str
from pipenv.utils.markers import normalize_marker_str
marker_keys = cls.parse_pyparsing_exprs(packaging_markers.VARIABLE)
markers = set()
@@ -217,7 +207,7 @@ class Entry:
@staticmethod
def marker_to_str(marker):
from pipenv.vendor.requirementslib.models.markers import normalize_marker_str
from pipenv.utils.markers import normalize_marker_str
if not marker:
return None
@@ -242,20 +232,19 @@ class Entry:
entry_extras = list(self.entry.extras)
if self.lockfile_entry.extras:
entry_extras.extend(list(self.lockfile_entry.extras))
self._entry.req.extras = entry_extras
self.entry_dict["extras"] = self.entry.extras
self.entry_dict["extras"] = entry_extras
if self.original_markers and not self.markers:
original_markers = self.marker_to_str(self.original_markers)
self.markers = original_markers
self.entry_dict["markers"] = self.marker_to_str(original_markers)
entry_hashes = set(self.entry.hashes)
locked_hashes = set(self.lockfile_entry.hashes)
if entry_hashes != locked_hashes and not self.is_updated:
self.entry_dict["hashes"] = sorted(entry_hashes | locked_hashes)
entry_hashes = set(self.entry_dict.get("hashes", []))
self.entry_dict["hashes"] = sorted(entry_hashes)
self.entry_dict["name"] = self.name
if "version" in self.entry_dict:
self.entry_dict["version"] = self.strip_version(self.entry_dict["version"])
_, self.entry_dict = self.get_markers_from_dict(self.entry_dict)
if self.resolver.index_lookup.get(self.name):
self.entry_dict["index"] = self.resolver.index_lookup[self.name]
return self.entry_dict
@property
@@ -383,12 +372,12 @@ class Entry:
@property
def updated_version(self):
version = self.entry.specifiers
version = str(self.entry.specifier)
return self.strip_version(version)
@property
def updated_specifier(self) -> str:
return self.entry.specifiers
return str(self.entry.specifier)
@property
def original_specifier(self) -> str:
@@ -452,15 +441,7 @@ class Entry:
:return: A set of **InstallRequirement** instances representing constraints
:rtype: Set
"""
constraints = {
c for c in self.resolver.parsed_constraints if c and c.name == self.entry.name
}
pipfile_constraint = self.get_pipfile_constraint()
if pipfile_constraint and not (
self.pipfile_entry.editable or pipfile_constraint.editable
):
constraints.add(pipfile_constraint)
return constraints
return self.resolver.parsed_constraints
def get_pipfile_constraint(self):
"""
@@ -470,7 +451,7 @@ class Entry:
:return: An **InstallRequirement** instance representing a version constraint
"""
if self.is_in_pipfile:
return self.pipfile_entry.ireq
return self.pipfile_entry
def validate_constraints(self):
"""
@@ -481,22 +462,24 @@ class Entry:
:raises: :exc:`pipenv.exceptions.DependencyConflict` if the constraints dont exist
"""
from pipenv.exceptions import DependencyConflict
from pipenv.patched.pip._vendor.packaging.requirements import Requirement
from pipenv.utils import err
constraints = self.get_constraints()
pinned_version = self.updated_version
for constraint in constraints:
if not constraint.req:
if not isinstance(constraint, Requirement):
continue
if pinned_version and not constraint.req.specifier.contains(
if pinned_version and not constraint.specifier.contains(
str(pinned_version), prereleases=True
):
if self.project.s.is_verbose():
print(f"Tried constraint: {constraint!r}", file=sys.stderr)
err.print(f"Tried constraint: {constraint!r}")
msg = (
"Cannot resolve conflicting version {}{} while {}{} is "
"locked.".format(
self.name,
constraint.req.specifier,
constraint.specifier,
self.name,
self.updated_specifier,
)
@@ -582,37 +565,6 @@ def clean_results(results, resolver, project, category):
return new_results
def parse_packages(packages, pre, clear, system, requirements_dir=None):
from pipenv.utils.indexes import parse_indexes
from pipenv.vendor.requirementslib.fileutils import cd, temp_path
from pipenv.vendor.requirementslib.models.requirements import Requirement
parsed_packages = []
for package in packages:
*_, line = parse_indexes(package)
line = " ".join(line)
pf = {}
req = Requirement.from_line(line)
if not req.name:
with temp_path(), cd(req.req.setup_info.base_dir):
sys.path.insert(0, req.req.setup_info.base_dir)
req.req.setup_info.get_info()
req.update_name_from_path(req.req.setup_info.base_dir)
try:
name, entry = req.pipfile_entry
except Exception:
continue
else:
if name is not None and entry is not None:
pf[name] = entry
parsed_packages.append(pf)
print("RESULTS:")
if parsed_packages:
print(json.dumps(parsed_packages))
else:
print(json.dumps([]))
def resolve_packages(
pre,
clear,
@@ -633,8 +585,12 @@ def resolve_packages(
else None
)
if not isinstance(packages, set):
packages = set(packages)
if not isinstance(constraints, set):
constraints = set(constraints) if constraints else set()
if constraints:
packages += constraints
packages |= constraints
def resolve(
packages, pre, project, sources, clear, system, category, requirements_dir=None
@@ -692,31 +648,15 @@ def _main(
parse_only=False,
category=None,
):
if parse_only:
parse_packages(
packages,
pre=pre,
clear=clear,
system=system,
requirements_dir=requirements_dir,
)
else:
resolve_packages(
pre, clear, verbose, system, write, requirements_dir, packages, category
)
resolve_packages(
pre, clear, verbose, system, write, requirements_dir, packages, category
)
def main(argv=None):
parser = get_parser()
parsed, remaining = parser.parse_known_args(argv)
_ensure_modules()
import warnings
from pipenv.vendor.click.utils import get_text_stream
warnings.simplefilter("ignore", category=ResourceWarning)
sys.stdout = get_text_stream("stdout")
sys.stderr = get_text_stream("stderr")
os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
os.environ["PYTHONIOENCODING"] = "utf-8"
os.environ["PYTHONUNBUFFERED"] = "1"
+1 -1
View File
@@ -1,8 +1,8 @@
import shutil
from pipenv import environments
from pipenv.utils.funktools import handle_remove_readonly
from pipenv.vendor import click
from pipenv.vendor.requirementslib.models.setup_info import handle_remove_readonly
def do_clear(project):
+76 -158
View File
@@ -9,6 +9,11 @@ from pipenv import environments, exceptions
from pipenv.patched.pip._internal.exceptions import PipError
from pipenv.patched.pip._vendor import rich
from pipenv.routines.lock import do_lock
from pipenv.utils import fileutils
from pipenv.utils.dependencies import (
expansive_install_req_from_line,
get_lockfile_section_using_pipfile_category,
)
from pipenv.utils.indexes import get_source_list
from pipenv.utils.internet import download_file, is_valid_url
from pipenv.utils.pip import (
@@ -18,11 +23,9 @@ from pipenv.utils.pip import (
from pipenv.utils.pipfile import ensure_pipfile
from pipenv.utils.project import ensure_project
from pipenv.utils.requirements import add_index_to_pipfile, import_requirements
from pipenv.utils.shell import temp_environ
from pipenv.utils.virtualenv import cleanup_virtualenv, do_create_virtualenv
from pipenv.vendor import click
from pipenv.vendor.requirementslib import fileutils
from pipenv.vendor.requirementslib.models.requirements import Requirement
from pipenv.vendor.requirementslib.utils import temp_environ
console = rich.console.Console()
err = rich.console.Console(stderr=True)
@@ -32,7 +35,7 @@ def do_install(
project,
packages=False,
editable_packages=False,
index_url=False,
index=False,
dev=False,
python=False,
pypi_mirror=None,
@@ -123,7 +126,6 @@ def do_install(
fd.close()
# Replace the url with the temporary requirements file
requirementstxt = temp_reqs
remote = True
if requirementstxt:
error, traceback = None, None
click.secho(
@@ -140,7 +142,7 @@ def do_install(
)
except (UnicodeDecodeError, PipError) as e:
# Don't print the temp file path if remote since it will be deleted.
req_path = requirements_url if remote else project.path_to(requirementstxt)
req_path = project.path_to(requirementstxt)
error = (
"Unexpected syntax in {}. Are you sure this is a "
"requirements.txt style file?".format(req_path)
@@ -153,10 +155,6 @@ def do_install(
)
traceback = e
finally:
# If requirements file was provided by remote url delete the temporary file
if remote:
fd.close() # Close for windows to allow file cleanup.
os.remove(temp_reqs)
if error and traceback:
click.secho(error, fg="red")
click.secho(str(traceback), fg="yellow", err=True)
@@ -188,8 +186,6 @@ def do_install(
# This is for if the user passed in dependencies, then we want to make sure we
else:
from pipenv.vendor.requirementslib.models.requirements import Requirement
# make a tuple of (display_name, entry)
pkg_list = packages + [f"-e {pkg}" for pkg in editable_packages]
if not system and not project.virtualenv_exists:
@@ -219,9 +215,11 @@ def do_install(
os.environ["PIP_USER"] = "0"
if "PYTHONHOME" in os.environ:
del os.environ["PYTHONHOME"]
st.console.print(f"Resolving {pkg_line}...")
st.console.print(f"Resolving {pkg_line}...", markup=False)
try:
pkg_requirement = Requirement.from_line(pkg_line)
pkg_requirement = expansive_install_req_from_line(
pkg_line, expand_env=True
)
except ValueError as e:
err.print("{}: {}".format(click.style("WARNING", fg="red"), e))
err.print(
@@ -233,7 +231,8 @@ def do_install(
st.update(f"Installing {pkg_requirement.name}...")
# Warn if --editable wasn't passed.
if (
pkg_requirement.is_vcs
pkg_requirement.link
and pkg_requirement.link.is_vcs
and not pkg_requirement.editable
and not project.s.PIPENV_RESOLVE_VCS
):
@@ -254,25 +253,38 @@ def do_install(
pipfile_sections = "[dev-packages]"
else:
pipfile_sections = "[packages]"
st.console.print(
f"[bold]Adding [green]{pkg_requirement.name}[/green][/bold] to Pipfile's [yellow]\\{pipfile_sections}[/yellow] ..."
)
# Add the package to the Pipfile.
if index_url:
index_name = add_index_to_pipfile(project, index_url)
pkg_requirement.index = index_name
if index:
source = project.get_index_by_name(index)
default_index = project.get_default_index()["name"]
if not source:
index_name = add_index_to_pipfile(project, index)
if index_name != default_index:
pkg_requirement.index = index_name
elif source["name"] != default_index:
pkg_requirement.index = source["name"]
try:
if categories:
for category in categories:
added, cat = project.add_package_to_pipfile(
pkg_requirement, dev, category
added, cat, normalized_name = project.add_package_to_pipfile(
pkg_requirement, pkg_line, dev, category
)
if added:
new_packages.append((pkg_requirement.name, cat))
new_packages.append((normalized_name, cat))
st.console.print(
f"[bold]Added [green]{normalized_name}[/green][/bold] to Pipfile's "
f"[yellow]\\{pipfile_sections}[/yellow] ..."
)
else:
added, cat = project.add_package_to_pipfile(pkg_requirement, dev)
added, cat, normalized_name = project.add_package_to_pipfile(
pkg_requirement, pkg_line, dev
)
if added:
new_packages.append((pkg_requirement.name, cat))
new_packages.append((normalized_name, cat))
st.console.print(
f"[bold]Added [green]{normalized_name}[/green][/bold] to Pipfile's "
f"[yellow]\\{pipfile_sections}[/yellow] ..."
)
except ValueError:
import traceback
@@ -306,11 +318,11 @@ def do_install(
extra_pip_args=extra_pip_args,
categories=categories,
)
except RuntimeError:
except Exception as e:
# If we fail to install, remove the package from the Pipfile.
for pkg_name, category in new_packages:
project.remove_package_from_pipfile(pkg_name, category)
sys.exit(1)
raise e
sys.exit(0)
@@ -393,8 +405,6 @@ def do_install_dependencies(
else:
categories = ["packages"]
lockfile = None
pipfile = None
for category in categories:
lockfile = project.get_or_create_lockfile(categories=categories)
if not bare:
@@ -405,18 +415,18 @@ def do_install_dependencies(
bold=True,
)
dev = dev or dev_only
if lockfile:
deps_list = list(
lockfile.get_requirements(dev=dev, only=dev_only, categories=[category])
)
else:
deps_list = []
for req_name, specifier in pipfile.items():
deps_list.append(Requirement.from_pipfile(req_name, specifier))
failed_deps_queue = queue.Queue()
deps_list = list(
lockfile.get_requirements(dev=dev, only=dev_only, categories=[category])
)
editable_or_vcs_deps = [
(dep, pip_line) for dep, pip_line in deps_list if (dep.link and dep.editable)
]
normal_deps = [
(dep, pip_line)
for dep, pip_line in deps_list
if not (dep.link and dep.editable)
]
editable_or_vcs_deps = [dep for dep in deps_list if (dep.editable or dep.vcs)]
normal_deps = [dep for dep in deps_list if not (dep.editable or dep.vcs)]
install_kwargs = {
"no_deps": True,
"ignore_hashes": ignore_hashes,
@@ -425,51 +435,19 @@ def do_install_dependencies(
"sequential_deps": editable_or_vcs_deps,
"extra_pip_args": extra_pip_args,
}
lockfile_category = get_lockfile_section_using_pipfile_category(category)
lockfile_section = lockfile[lockfile_category]
batch_install(
project,
normal_deps,
lockfile_section,
procs,
failed_deps_queue,
requirements_dir,
**install_kwargs,
)
if not procs.empty():
_cleanup_procs(project, procs, failed_deps_queue)
# Iterate over the hopefully-poorly-packaged dependencies...
if not failed_deps_queue.empty():
click.secho("Installing initially failed dependencies...", bold=True)
retry_list = []
while not failed_deps_queue.empty():
failed_dep = failed_deps_queue.get()
retry_list.append(failed_dep)
install_kwargs.update({"retry": False})
batch_install(
project,
retry_list,
procs,
failed_deps_queue,
requirements_dir,
**install_kwargs,
)
if not procs.empty():
_cleanup_procs(project, procs, failed_deps_queue, retry=False)
if not failed_deps_queue.empty():
failed_list = []
while not failed_deps_queue.empty():
failed_dep = failed_deps_queue.get()
failed_list.append(failed_dep)
click.echo(
click.style(
f"Failed to install some dependency or packages. "
f"The following have failed installation and attempted retry: {failed_list}",
fg="red",
),
err=True,
)
sys.exit(1)
_cleanup_procs(project, procs)
def batch_install_iteration(
@@ -477,33 +455,12 @@ def batch_install_iteration(
deps_to_install,
sources,
procs,
failed_deps_queue,
requirements_dir,
no_deps=True,
ignore_hashes=False,
allow_global=False,
retry=True,
extra_pip_args=None,
):
from pipenv.vendor.requirementslib.models.utils import (
strip_extras_markers_from_requirement,
)
is_artifact = False
for dep in deps_to_install:
if dep.req.req:
dep.req.req = strip_extras_markers_from_requirement(dep.req.req)
if dep.markers:
dep.markers = str(strip_extras_markers_from_requirement(dep.get_markers))
# Install the module.
if dep.is_file_or_url and (
dep.is_direct_url
or any(dep.req.uri.endswith(ext) for ext in ["zip", "tar.gz"])
):
is_artifact = True
elif dep.is_vcs:
is_artifact = True
with temp_environ():
if not allow_global:
os.environ["PIP_USER"] = "0"
@@ -511,10 +468,6 @@ def batch_install_iteration(
del os.environ["PYTHONHOME"]
if "GIT_CONFIG" in os.environ:
del os.environ["GIT_CONFIG"]
use_pep517 = True
if not retry and not is_artifact:
use_pep517 = False
cmds = pip_install_deps(
project,
deps=deps_to_install,
@@ -523,26 +476,25 @@ def batch_install_iteration(
ignore_hashes=ignore_hashes,
no_deps=no_deps,
requirements_dir=requirements_dir,
use_pep517=use_pep517,
use_pep517=True,
extra_pip_args=extra_pip_args,
)
for c in cmds:
procs.put(c)
_cleanup_procs(project, procs, failed_deps_queue, retry=retry)
_cleanup_procs(project, procs)
def batch_install(
project,
deps_list,
lockfile_section,
procs,
failed_deps_queue,
requirements_dir,
no_deps=True,
ignore_hashes=False,
allow_global=False,
pypi_mirror=None,
retry=True,
sequential_deps=None,
extra_pip_args=None,
):
@@ -551,7 +503,9 @@ def batch_install(
deps_to_install = deps_list[:]
deps_to_install.extend(sequential_deps)
deps_to_install = [
dep for dep in deps_to_install if not project.environment.is_satisfied(dep)
(dep, pip_line)
for dep, pip_line in deps_to_install
if not project.environment.is_satisfied(dep)
]
search_all_sources = project.settings.get("install_search_all_sources", False)
sources = get_source_list(
@@ -562,27 +516,26 @@ def batch_install(
pypi_mirror=pypi_mirror,
)
if search_all_sources:
dependencies = [pip_line for _, pip_line in deps_to_install]
batch_install_iteration(
project,
deps_to_install,
dependencies,
sources,
procs,
failed_deps_queue,
requirements_dir,
no_deps=no_deps,
ignore_hashes=ignore_hashes,
allow_global=allow_global,
retry=retry,
extra_pip_args=extra_pip_args,
)
else:
# Sort the dependencies out by index -- include editable/vcs in the default group
deps_by_index = defaultdict(list)
for dependency in deps_to_install:
if dependency.index:
deps_by_index[dependency.index].append(dependency)
else:
deps_by_index[project.sources_default["name"]].append(dependency)
for dependency, pip_line in deps_to_install:
index = project.sources_default["name"]
if dependency.name and lockfile_section[dependency.name].get("index"):
index = lockfile_section[dependency.name]["index"]
deps_by_index[index].append(pip_line)
# Treat each index as its own pip install phase
for index_name, dependencies in deps_by_index.items():
try:
@@ -592,12 +545,10 @@ def batch_install(
dependencies,
[install_source],
procs,
failed_deps_queue,
requirements_dir,
no_deps=no_deps,
ignore_hashes=ignore_hashes,
allow_global=allow_global,
retry=retry,
extra_pip_args=extra_pip_args,
)
except StopIteration:
@@ -609,7 +560,7 @@ def batch_install(
sys.exit(1)
def _cleanup_procs(project, procs, failed_deps_queue, retry=True):
def _cleanup_procs(project, procs):
while not procs.empty():
c = procs.get()
try:
@@ -621,47 +572,14 @@ def _cleanup_procs(project, procs, failed_deps_queue, retry=True):
click.secho(out.strip() or err.strip(), fg="yellow")
# The Installation failed...
if failed:
# The Installation failed...
# We echo both c.stdout and c.stderr because pip returns error details on out.
err = err.strip().splitlines() if err else []
out = out.strip().splitlines() if out else []
err_lines = [line for message in [out, err] for line in message]
deps = getattr(c, "deps", {}).copy()
for dep in deps:
# If there is a mismatch in installed locations or the install fails
# due to wrongful disabling of pep517, we should allow for
# additional passes at installation
if "does not match installed location" in err:
project.environment.expand_egg_links()
click.echo(
"{}".format(
click.style(
"Failed initial installation: Failed to overwrite existing "
"package, likely due to path aliasing. Expanding and trying "
"again!",
fg="yellow",
)
)
)
if dep:
dep.use_pep517 = True
elif "Disabling PEP 517 processing is invalid" in err:
if dep:
dep.use_pep517 = True
elif not retry:
# The Installation failed...
# We echo both c.stdout and c.stderr because pip returns error details on out.
err = err.strip().splitlines() if err else []
out = out.strip().splitlines() if out else []
err_lines = [line for message in [out, err] for line in message]
# Return the subprocess' return code.
raise exceptions.InstallError(deps, extra=err_lines)
else:
# Alert the user.
click.echo(
"{} {}! Will try again.".format(
click.style("An error occurred while installing", fg="red"),
click.style(dep.as_line() if dep else "", fg="green"),
),
err=True,
)
# Save the Failed Dependency for later.
failed_deps_queue.put(dep)
# Return the subprocess' return code.
raise exceptions.InstallError(deps, extra=err_lines)
def do_init(
+6 -5
View File
@@ -29,10 +29,10 @@ def do_lock(
lockfile_categories.insert(0, "default")
# Create the lockfile.
lockfile = project.lockfile(categories=lockfile_categories)
for category in lockfile_categories:
for k, v in lockfile.get(category, {}).copy().items():
if not hasattr(v, "keys"):
del lockfile[category][k]
# for category in lockfile_categories:
# for k, v in lockfile.get(category, {}).copy().items():
# if not hasattr(v, "keys"):
# del lockfile[category][k]
# Resolve package to generate constraints before resolving other categories
for category in lockfile_categories:
@@ -55,7 +55,7 @@ def do_lock(
# Prune old lockfile category as new one will be created.
try:
del lockfile[category]
old_lock_data = lockfile.pop(category)
except KeyError:
pass
@@ -73,6 +73,7 @@ def do_lock(
pypi_mirror=pypi_mirror,
pipfile=packages,
lockfile=lockfile,
old_lock_data=old_lock_data,
)
# Overwrite any category packages with default packages.
+11 -5
View File
@@ -4,10 +4,13 @@ from collections.abc import Mapping
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.routines.lock import do_lock
from pipenv.utils.dependencies import pep423_name
from pipenv.utils.dependencies import (
as_pipfile,
expansive_install_req_from_line,
get_version,
pep423_name,
)
from pipenv.vendor import click
from pipenv.vendor.requirementslib.models.requirements import Requirement
from pipenv.vendor.requirementslib.models.utils import get_version
def do_outdated(project, pypi_mirror=None, pre=False, clear=False):
@@ -26,8 +29,11 @@ def do_outdated(project, pypi_mirror=None, pre=False, clear=False):
for name, deps in project.environment.reverse_dependencies().items()
}
for result in installed_packages:
dep = Requirement.from_line(str(result.as_requirement()))
packages.update(dep.as_pipfile())
dep = expansive_install_req_from_line(
str(result.as_requirement()), expand_env=True
)
packages.update(as_pipfile(dep))
updated_packages = {}
lockfile = do_lock(
project, clear=clear, pre=pre, write=False, pypi_mirror=pypi_mirror
+2 -53
View File
@@ -2,61 +2,10 @@ import re
import sys
from pipenv.utils.dependencies import get_lockfile_section_using_pipfile_category
from pipenv.utils.requirements import requirements_from_lockfile
from pipenv.vendor import click
def requirements_from_deps(deps, include_hashes=True, include_markers=True):
pip_packages = []
for package_name, package_info in deps.items():
# Handling git repositories
if "git" in package_info:
git = package_info["git"]
ref = package_info.get("ref", "")
extras = (
"[{}]".format(",".join(package_info.get("extras", [])))
if "extras" in package_info
else ""
)
pip_package = f"{package_name}{extras} @ git+{git}@{ref}"
# Handling file-sourced packages
elif "file" in package_info or "path" in package_info:
file = package_info.get("file") or package_info.get("path")
extras = (
"[{}]".format(",".join(package_info.get("extras", [])))
if "extras" in package_info
else ""
)
pip_package = f"{file}{extras}"
else:
# Handling packages from standard pypi like indexes
version = package_info.get("version", "").replace("==", "")
hashes = (
" --hash={}".format(" --hash=".join(package_info["hashes"]))
if include_hashes and "hashes" in package_info
else ""
)
markers = (
"; {}".format(package_info["markers"])
if include_markers
and "markers" in package_info
and package_info["markers"]
else ""
)
extras = (
"[{}]".format(",".join(package_info.get("extras", [])))
if "extras" in package_info
else ""
)
pip_package = f"{package_name}{extras}=={version}{markers}{hashes}"
# Append to the list
pip_packages.append(pip_package)
# pip_packages contains the pip-installable lines
return pip_packages
def generate_requirements(
project,
dev=False,
@@ -84,7 +33,7 @@ def generate_requirements(
if not dev_only:
deps.update(lockfile["default"])
pip_installable_lines = requirements_from_deps(
pip_installable_lines = requirements_from_lockfile(
deps, include_hashes=include_hashes, include_markers=include_markers
)
+2 -2
View File
@@ -6,6 +6,7 @@ from pipenv.patched.pip._internal.build_env import get_runnable_pip
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.routines.lock import do_lock
from pipenv.utils.dependencies import (
expansive_install_req_from_line,
get_canonical_names,
get_lockfile_section_using_pipfile_category,
get_pipfile_category_using_lockfile_section,
@@ -16,7 +17,6 @@ from pipenv.utils.project import ensure_project
from pipenv.utils.requirements import BAD_PACKAGES
from pipenv.utils.shell import cmd_list_to_shell, project_python
from pipenv.vendor import click
from pipenv.vendor.requirementslib import Requirement
def do_uninstall(
@@ -43,7 +43,7 @@ def do_uninstall(
if not categories:
categories = project.get_package_categories(for_lockfile=True)
editable_pkgs = [
Requirement.from_line(f"-e {p}").name for p in editable_packages if p
expansive_install_req_from_line(f"-e {p}").name for p in editable_packages if p
]
packages += editable_pkgs
package_names = {p for p in packages if p}
+52 -61
View File
@@ -1,19 +1,17 @@
import sys
from collections import defaultdict
from pipenv.routines.install import do_sync
from pipenv.routines.lock import do_lock
from pipenv.routines.outdated import do_outdated
from pipenv.utils.dependencies import (
convert_deps_to_pip,
expansive_install_req_from_line,
get_pipfile_category_using_lockfile_section,
is_star,
pep423_name,
)
from pipenv.utils.project import ensure_project
from pipenv.utils.requirements import add_index_to_pipfile
from pipenv.utils.resolver import venv_resolve_deps
from pipenv.vendor import click
from pipenv.vendor.requirementslib.models.requirements import Requirement
def do_update(
@@ -44,15 +42,6 @@ def do_update(
site_packages=site_packages,
clear=clear,
)
if not outdated:
outdated = bool(dry_run)
if outdated:
do_outdated(
project,
clear=clear,
pre=pre,
pypi_mirror=pypi_mirror,
)
packages = [p for p in packages if p]
editable = [p for p in editable_packages if p]
if not packages:
@@ -86,6 +75,16 @@ def do_update(
lock_only=lock_only,
)
if not outdated:
outdated = bool(dry_run)
if outdated:
do_outdated(
project,
clear=clear,
pre=pre,
pypi_mirror=pypi_mirror,
)
do_sync(
project,
dev=dev,
@@ -120,67 +119,59 @@ def upgrade(
elif not categories:
categories = ["default"]
package_args = [p for p in packages] + [f"-e {pkg}" for pkg in editable_packages]
index_name = None
if index_url:
index_name = add_index_to_pipfile(project, index_url)
reqs = {}
requested_packages = {}
for package in package_args[:]:
# section = project.packages if not dev else project.dev_packages
section = {}
package = Requirement.from_line(package)
if index_name:
package.index = index_name
package_name, package_val = package.pipfile_entry
package_name = pep423_name(package_name)
requested_packages[package_name] = package
try:
if not is_star(section[package_name]) and is_star(package_val):
# Support for VCS dependencies.
package_val = convert_deps_to_pip(
{package_name: section[package_name]}, project=project
)[0]
except KeyError:
pass
reqs[package_name] = package_val
package_args = [p for p in packages] + [f"-e {pkg}" for pkg in editable_packages]
if not reqs:
click.echo("Nothing to upgrade!")
sys.exit(0)
# Resolve package to generate constraints of new package data
upgrade_lock_data = venv_resolve_deps(
reqs,
which=project._which,
project=project,
lockfile={},
category="default",
pre=pre,
allow_global=system,
pypi_mirror=pypi_mirror,
)
if not upgrade_lock_data:
click.echo("Nothing to upgrade!")
sys.exit(0)
# Upgrade the relevant packages in the various categories specified
requested_install_reqs = defaultdict(dict)
requested_packages = defaultdict(dict)
for category in categories:
pipfile_category = get_pipfile_category_using_lockfile_section(category)
for package in package_args[:]:
install_req = expansive_install_req_from_line(package, expand_env=True)
if index_name:
install_req.index = index_name
name, normalized_name, pipfile_entry = project.generate_package_pipfile_entry(
install_req, package, category=pipfile_category
)
project.add_pipfile_entry_to_pipfile(
name, normalized_name, pipfile_entry, category=pipfile_category
)
requested_packages[pipfile_category][normalized_name] = pipfile_entry
requested_install_reqs[pipfile_category][normalized_name] = install_req
if project.pipfile_exists:
packages = project.parsed_pipfile.get(pipfile_category, {})
else:
packages = project.get_pipfile_section(pipfile_category)
for package_name, requirement in requested_packages.items():
requested_package = reqs[package_name]
if not package_args:
click.echo("Nothing to upgrade!")
sys.exit(0)
# Resolve package to generate constraints of new package data
upgrade_lock_data = venv_resolve_deps(
requested_packages[pipfile_category],
which=project._which,
project=project,
lockfile={},
category="default",
pre=pre,
allow_global=system,
pypi_mirror=pypi_mirror,
)
if not upgrade_lock_data:
click.echo("Nothing to upgrade!")
sys.exit(0)
for package_name, pipfile_entry in requested_packages[pipfile_category].items():
if package_name not in packages:
packages.append(package_name, requested_package)
packages.append(package_name, pipfile_entry)
else:
packages[package_name] = requested_package
if lock_only is False:
project.add_package_to_pipfile(requirement, category=pipfile_category)
packages[package_name] = pipfile_entry
full_lock_resolution = venv_resolve_deps(
packages,
+1 -1
View File
@@ -8,8 +8,8 @@ import sys
from pathlib import Path
from shutil import get_terminal_size
from pipenv.utils.shell import temp_environ
from pipenv.vendor import shellingham
from pipenv.vendor.requirementslib.utils import temp_environ
ShellDetectionFailure = shellingham.ShellDetectionFailure
+1 -1
View File
@@ -2,7 +2,7 @@ import logging
from pipenv.patched.pip._vendor.rich.console import Console
logging.basicConfig(level=logging.ERROR)
logging.basicConfig(level=logging.INFO)
console = Console()
err = Console(stderr=True)
+32
View File
@@ -3,6 +3,38 @@ VCS_LIST = ("git", "svn", "hg", "bzr")
SCHEME_LIST = ("http://", "https://", "ftp://", "ftps://", "file://")
FALSE_VALUES = ("0", "false", "no", "off")
TRUE_VALUES = ("1", "true", "yes", "on")
REMOTE_FILE_SCHEMES = [
"http",
"https",
"ftp",
]
VCS_SCHEMES = [
"git+http",
"git+https",
"git+ssh",
"git+git",
"hg+http",
"hg+https",
"hg+ssh",
"svn+http",
"svn+https",
"svn+svn",
"bzr+http",
"bzr+https",
"bzr+ssh",
"bzr+sftp",
"bzr+ftp",
"bzr+lp",
]
REMOTE_SCHEMES = REMOTE_FILE_SCHEMES + VCS_SCHEMES
RELEVANT_PROJECT_FILES = (
"METADATA",
"PKG-INFO",
"setup.py",
"setup.cfg",
"pyproject.toml",
)
def is_type_checking():
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function
import errno
import os
import sys
@@ -13,7 +10,7 @@ class RequirementError(Exception):
class MissingParameter(Exception):
def __init__(self, param):
self.message = self.get_message(param)
super(MissingParameter, self).__init__(self.message)
super().__init__(self.message)
@classmethod
def get_message(cls, param):
@@ -25,7 +22,6 @@ class MissingParameter(Exception):
class FileCorruptException(OSError):
def __init__(self, path, *args, **kwargs):
path = path
backup_path = kwargs.pop("backup_path", None)
if not backup_path and args:
args = reversed(args)
@@ -38,7 +34,7 @@ class FileCorruptException(OSError):
if args:
args = reversed(args)
self.message = self.get_message(path, backup_path=backup_path)
super(FileCorruptException, self).__init__(self.message)
super().__init__(self.message)
def get_message(self, path, backup_path=None):
message = "ERROR: Failed to load file at %s" % path
@@ -46,7 +42,7 @@ class FileCorruptException(OSError):
msg = "it will be backed up to %s and removed" % backup_path
else:
msg = "it will be removed and replaced on the next lock."
message = "{0}\nYour lockfile is corrupt, {1}".format(message, msg)
message = f"{message}\nYour lockfile is corrupt, {msg}"
return message
def show(self):
@@ -56,7 +52,7 @@ class FileCorruptException(OSError):
class LockfileCorruptException(FileCorruptException):
def __init__(self, path, backup_path=None):
self.message = self.get_message(path, backup_path=backup_path)
super(LockfileCorruptException, self).__init__(self.message)
super().__init__(self.message)
def get_message(self, path, backup_path=None):
message = "ERROR: Failed to load lockfile at %s" % path
@@ -64,7 +60,7 @@ class LockfileCorruptException(FileCorruptException):
msg = "it will be backed up to %s and removed" % backup_path
else:
msg = "it will be removed and replaced on the next lock."
message = "{0}\nYour lockfile is corrupt, {1}".format(message, msg)
message = f"{message}\nYour lockfile is corrupt, {msg}"
return message
def show(self, path, backup_path=None):
@@ -74,7 +70,7 @@ class LockfileCorruptException(FileCorruptException):
class PipfileCorruptException(FileCorruptException):
def __init__(self, path, backup_path=None):
self.message = self.get_message(path, backup_path=backup_path)
super(PipfileCorruptException, self).__init__(self.message)
super().__init__(self.message)
def get_message(self, path, backup_path=None):
message = "ERROR: Failed to load Pipfile at %s" % path
@@ -82,7 +78,7 @@ class PipfileCorruptException(FileCorruptException):
msg = "it will be backed up to %s and removed" % backup_path
else:
msg = "it will be removed and replaced on the next lock."
message = "{0}\nYour Pipfile is corrupt, {1}".format(message, msg)
message = f"{message}\nYour Pipfile is corrupt, {msg}"
return message
def show(self, path, backup_path=None):
@@ -93,4 +89,4 @@ class PipfileNotFound(FileNotFoundError):
def __init__(self, path, *args, **kwargs):
self.errno = errno.ENOENT
self.filename = path
super(PipfileNotFound, self).__init__(self.filename)
super().__init__(self.filename)
@@ -1,51 +1,25 @@
"""A collection for utilities for working with files and paths."""
import atexit
import io
import os
import posixpath
import sys
import warnings
from contextlib import closing, contextmanager
from http.client import HTTPResponse as Urllib_HTTPResponse
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import IO, Any, ContextManager, Iterator, Optional, Text, TypeVar, Union
from typing import IO, Any, ContextManager, Optional, TypeVar, Union
from urllib import parse as urllib_parse
from urllib import request as urllib_request
from urllib.parse import quote, urlparse
from pipenv.patched.pip._vendor.requests import Session
from pipenv.patched.pip._vendor.urllib3.response import HTTPResponse as Urllib3_HTTPResponse
from pipenv.patched.pip._vendor.urllib3.response import (
HTTPResponse as Urllib3_HTTPResponse,
)
_T = TypeVar("_T")
@contextmanager
def cd(path):
# type: () -> Iterator[None]
"""Context manager to temporarily change working directories.
:param str path: The directory to move into
>>> print(os.path.abspath(os.curdir))
'/home/user/code/myrepo'
>>> with cd("/home/user/code/otherdir/subdir"):
... print("Changed directory: %s" % os.path.abspath(os.curdir))
Changed directory: /home/user/code/otherdir/subdir
>>> print(os.path.abspath(os.curdir))
'/home/user/code/myrepo'
"""
if not path:
return
prev_cwd = Path.cwd().as_posix()
if isinstance(path, Path):
path = path.as_posix()
os.chdir(str(path))
try:
yield
finally:
os.chdir(prev_cwd)
def is_file_url(url: Any) -> bool:
"""Returns true if the given url is a file url."""
if not url:
@@ -54,7 +28,7 @@ def is_file_url(url: Any) -> bool:
try:
url = url.url
except AttributeError:
raise ValueError("Cannot parse url from unknown type: {!r}".format(url))
raise ValueError(f"Cannot parse url from unknown type: {url!r}")
return urllib_parse.urlparse(url.lower()).scheme == "file"
@@ -83,7 +57,7 @@ if os.name == "nt":
# from click _winconsole.py
from ctypes import create_unicode_buffer, windll
def get_long_path(short_path: Text) -> Text:
def get_long_path(short_path: str) -> str:
BUFFER_SIZE = 500
buffer = create_unicode_buffer(BUFFER_SIZE)
get_long_path_name = windll.kernel32.GetLongPathNameW
@@ -135,7 +109,7 @@ def path_to_url(path):
# XXX: actually part of a surrogate pair, but were just incidentally
# XXX: passed in as a piece of a filename
quoted_path = quote(path, errors="backslashreplace")
return "file:///{}:{}".format(drive, quoted_path)
return f"file:///{drive}:{quoted_path}"
# XXX: This is also here to help deal with incidental dangling surrogates
# XXX: on linux, by making sure they are preserved during encoding so that
# XXX: we can urlencode the backslash correctly
@@ -160,7 +134,7 @@ def open_file(
try:
link = link.url_without_fragment
except AttributeError:
raise ValueError("Cannot parse url from unknown type: {0!r}".format(link))
raise ValueError(f"Cannot parse url from unknown type: {link!r}")
if not is_valid_url(link) and os.path.exists(link):
link = path_to_url(link)
@@ -169,9 +143,9 @@ def open_file(
# Local URL
local_path = url_to_path(link)
if os.path.isdir(local_path):
raise ValueError("Cannot open directory for read: {}".format(link))
raise ValueError(f"Cannot open directory for read: {link}")
else:
with io.open(local_path, "rb") as local_file:
with open(local_path, "rb") as local_file:
yield local_file
else:
# Remote URL
@@ -264,52 +238,3 @@ def check_for_unc_path(path):
return True
else:
return False
def get_converted_relative_path(path, relative_to=None):
"""Convert `path` to be relative.
Given a vague relative path, return the path relative to the given
location.
:param str path: The location of a target path
:param str relative_to: The starting path to build against, optional
:returns: A relative posix-style path with a leading `./`
This performs additional conversion to ensure the result is of POSIX form,
and starts with `./`, or is precisely `.`.
>>> os.chdir('/home/user/code/myrepo/myfolder')
>>> vistir.path.get_converted_relative_path('/home/user/code/file.zip')
'./../../file.zip'
>>> vistir.path.get_converted_relative_path('/home/user/code/myrepo/myfolder/mysubfolder')
'./mysubfolder'
>>> vistir.path.get_converted_relative_path('/home/user/code/myrepo/myfolder')
'.'
"""
if not relative_to:
relative_to = os.getcwd()
start_path = Path(str(relative_to))
try:
start = start_path.resolve()
except OSError:
start = start_path.absolute()
# check if there is a drive letter or mount point
# if it is a mountpoint use the original absolute path
# instead of the unc path
if check_for_unc_path(start):
start = start_path.absolute()
path = start.joinpath(str(path)).relative_to(start)
# check and see if the path that was passed into the function is a UNC path
# and raise value error if it is not.
if check_for_unc_path(path):
raise ValueError("The path argument does not currently accept UNC paths")
relpath_s = posixpath.normpath(path.as_posix())
if not (relpath_s == "." or relpath_s.startswith("./")):
relpath_s = posixpath.join(".", relpath_s)
return relpath_s
+291 -6
View File
@@ -1,13 +1,19 @@
"""
A small collection of useful functional tools for working with iterables.
This module should be in requirementslib. Once we release a new version of requirementslib
we can remove this file and use the one in requirementslib.
"""
import errno
import locale
import os
import stat
import subprocess
import time
import warnings
from functools import partial
from itertools import islice, tee
from itertools import count, islice, tee
from typing import Any, Iterable
DIRECTORY_CLEANUP_TIMEOUT = 1.0
def _is_iterable(elem: Any) -> bool:
if getattr(elem, "__iter__", False) or isinstance(elem, Iterable):
@@ -35,7 +41,6 @@ def chunked(n: int, iterable: Iterable) -> Iterable:
def unnest(elem: Iterable) -> Any:
# type: (Iterable) -> Any
"""Flatten an arbitrarily nested iterable.
:param elem: An iterable to flatten
@@ -73,8 +78,288 @@ def unnest(elem: Iterable) -> Any:
def dedup(iterable: Iterable) -> Iterable:
# type: (Iterable) -> Iterable
"""Deduplicate an iterable object like iter(set(iterable)) but order-
preserved."""
return iter(dict.fromkeys(iterable))
def is_readonly_path(fn: os.PathLike) -> bool:
"""check if a provided path exists and is readonly.
permissions check is `bool(path.stat & stat.s_iread)` or `not
os.access(path, os.w_ok)`
"""
if os.path.exists(fn):
file_stat = os.stat(fn).st_mode
return not bool(file_stat & stat.s_iwrite) or not os.access(fn, os.w_ok)
return False
def _wait_for_files(path): # pragma: no cover
"""Retry with backoff up to 1 second to delete files from a directory.
:param str path: The path to crawl to delete files from
:return: A list of remaining paths or None
:rtype: Optional[List[str]]
"""
timeout = 0.001 # noqa:S101
remaining = []
while timeout < DIRECTORY_CLEANUP_TIMEOUT:
remaining = []
if os.path.isdir(path):
L = os.listdir(path)
for target in L:
_remaining = _wait_for_files(target)
if _remaining:
remaining.extend(_remaining)
continue
try:
os.unlink(path)
except FileNotFoundError as e:
if e.errno == errno.ENOENT:
return
except (OSError, PermissionError): # noqa:B014
time.sleep(timeout)
timeout *= 2
remaining.append(path)
else:
return
return remaining
def _walk_for_powershell(directory):
for _, dirs, files in os.walk(directory):
powershell = next(
iter(fn for fn in files if fn.lower() == "powershell.exe"), None
)
if powershell is not None:
return os.path.join(directory, powershell)
for subdir in dirs:
powershell = _walk_for_powershell(os.path.join(directory, subdir))
if powershell:
return powershell
return None
def _get_powershell_path():
paths = [
os.path.expandvars(r"%windir%\{0}\WindowsPowerShell").format(subdir)
for subdir in ("SysWOW64", "system32")
]
powershell_path = next(iter(_walk_for_powershell(pth) for pth in paths), None)
if not powershell_path:
powershell_path = subprocess.run(["where", "powershell"])
if powershell_path.stdout:
return powershell_path.stdout.strip()
def _get_sid_with_powershell():
powershell_path = _get_powershell_path()
if not powershell_path:
return None
args = [
powershell_path,
"-ExecutionPolicy",
"Bypass",
"-Command",
"Invoke-Expression '[System.Security.Principal.WindowsIdentity]::GetCurrent().user | Write-Host'",
]
sid = subprocess.run(args, capture_output=True)
return sid.stdout.strip()
def get_value_from_tuple(value, value_type):
try:
import winreg
except ImportError:
import _winreg as winreg
if value_type in (winreg.REG_SZ, winreg.REG_EXPAND_SZ):
if "\0" in value:
return value[: value.index("\0")]
return value
return None
def query_registry_value(root, key_name, value):
try:
import winreg
except ImportError:
import _winreg as winreg
try:
with winreg.OpenKeyEx(root, key_name, 0, winreg.KEY_READ) as key:
return get_value_from_tuple(*winreg.QueryValueEx(key, value))
except OSError:
return None
def _get_sid_from_registry():
try:
import winreg
except ImportError:
import _winreg as winreg
var_names = ("%USERPROFILE%", "%HOME%")
current_user_home = next(iter(os.path.expandvars(v) for v in var_names if v), None)
root, subkey = (
winreg.HKEY_LOCAL_MACHINE,
r"Software\Microsoft\Windows NT\CurrentVersion\ProfileList",
)
subkey_names = []
value = None
matching_key = None
try:
with winreg.OpenKeyEx(root, subkey, 0, winreg.KEY_READ) as key:
for i in count():
key_name = winreg.EnumKey(key, i)
subkey_names.append(key_name)
value = query_registry_value(
root, rf"{subkey}\{key_name}", "ProfileImagePath"
)
if value and value.lower() == current_user_home.lower():
matching_key = key_name
break
except OSError:
pass
if matching_key is not None:
return matching_key
def _get_current_user():
fns = (_get_sid_from_registry, _get_sid_with_powershell)
for fn in fns:
result = fn()
if result:
return result
return None
def _find_icacls_exe():
if os.name == "nt":
paths = [
os.path.expandvars(r"%windir%\{0}").format(subdir)
for subdir in ("system32", "SysWOW64")
]
for path in paths:
icacls_path = next(
iter(fn for fn in os.listdir(path) if fn.lower() == "icacls.exe"), None
)
if icacls_path is not None:
icacls_path = os.path.join(path, icacls_path)
return icacls_path
return None
def set_write_bit(fn: str) -> None:
"""Set read-write permissions for the current user on the target path. Fail
silently if the path doesn't exist.
:param str fn: The target filename or path
:return: None
"""
if not os.path.exists(fn):
return
file_stat = os.stat(fn).st_mode
os.chmod(fn, file_stat | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
if os.name == "nt":
user_sid = _get_current_user()
icacls_exe = _find_icacls_exe() or "icacls"
if user_sid:
c = subprocess.run(
[
icacls_exe,
f"''{fn}''",
"/grant",
f"{user_sid}:WD",
"/T",
"/C",
"/Q",
],
capture_output=True,
# 2020-06-12 Yukihiko Shinoda
# There are 3 way to get system default encoding in Stack Overflow.
# see: https://stackoverflow.com/questions/37506535/how-to-get-the-system-default-encoding-in-python-2-x
# I investigated these way by using Shift-JIS Windows.
# >>> import locale
# >>> locale.getpreferredencoding()
# "cp932" (Shift-JIS)
# >>> import sys
# >>> sys.getdefaultencoding()
# "utf-8"
# >>> sys.stdout.encoding
# "UTF8"
encoding=locale.getpreferredencoding(),
)
if not c.err and c.returncode == 0:
return
if not os.path.isdir(fn):
for path in [fn, os.path.dirname(fn)]:
try:
os.chflags(path, 0)
except AttributeError:
pass
return None
for root, dirs, files in os.walk(fn, topdown=False):
for dir_ in [os.path.join(root, d) for d in dirs]:
set_write_bit(dir_)
for file_ in [os.path.join(root, f) for f in files]:
set_write_bit(file_)
def handle_remove_readonly(func, path, exc):
"""Error handler for shutil.rmtree.
Windows source repo folders are read-only by default, so this error handler
attempts to set them as writeable and then proceed with deletion.
:param function func: The caller function
:param str path: The target path for removal
:param Exception exc: The raised exception
This function will call check :func:`is_readonly_path` before attempting to call
:func:`set_write_bit` on the target path and try again.
"""
PERM_ERRORS = (errno.EACCES, errno.EPERM, errno.ENOENT)
default_warning_message = "Unable to remove file due to permissions restriction: {!r}"
# split the initial exception out into its type, exception, and traceback
exc_type, exc_exception, exc_tb = exc
if is_readonly_path(path):
# Apply write permission and call original function
set_write_bit(path)
try:
func(path)
except (OSError, FileNotFoundError, PermissionError) as e: # pragma: no cover
if e.errno in PERM_ERRORS:
if e.errno == errno.ENOENT:
return
remaining = None
if os.path.isdir(path):
remaining = _wait_for_files(path)
if remaining:
warnings.warn(
default_warning_message.format(path),
ResourceWarning,
stacklevel=2,
)
else:
func(path, ignore_errors=True)
return
if exc_exception.errno in PERM_ERRORS:
set_write_bit(path)
remaining = _wait_for_files(path)
try:
func(path)
except (OSError, FileNotFoundError, PermissionError) as e: # noqa:B014
if e.errno in PERM_ERRORS:
if e.errno != errno.ENOENT: # File still exists
warnings.warn(
default_warning_message.format(path),
ResourceWarning,
stacklevel=2,
)
return
else:
raise exc_exception
+18 -3
View File
@@ -1,5 +1,6 @@
import os
import re
from html.parser import HTMLParser
from urllib.parse import urlparse
from pipenv.patched.pip._vendor import requests
@@ -7,7 +8,7 @@ from pipenv.patched.pip._vendor.requests.adapters import HTTPAdapter
from pipenv.patched.pip._vendor.urllib3 import util as urllib3_util
def _get_requests_session(max_retries=1, verify_ssl=True):
def get_requests_session(max_retries=1, verify_ssl=True):
"""Load requests lazily."""
pip_client_cert = os.environ.get("PIP_CLIENT_CERT")
requests_session = requests.Session()
@@ -46,7 +47,7 @@ def create_mirror_source(url, name):
def download_file(url, filename, max_retries=1):
"""Downloads file from url to a path with filename"""
r = _get_requests_session(max_retries).get(url, stream=True)
r = get_requests_session(max_retries).get(url, stream=True)
r.close()
if not r.ok:
raise OSError("Unable to download file")
@@ -116,7 +117,7 @@ def is_url_equal(url: str, other_url: str) -> bool:
def proper_case(package_name):
"""Properly case project name from pypi.org."""
# Hit the simple API.
r = _get_requests_session().get(
r = get_requests_session().get(
f"https://pypi.org/pypi/{package_name}/json", timeout=0.3, stream=True
)
r.close()
@@ -128,3 +129,17 @@ def proper_case(package_name):
good_name = match.group(1)
return good_name
class PackageIndexHTMLParser(HTMLParser):
def __init__(self):
super().__init__()
self.urls = []
def handle_starttag(self, tag, attrs):
# If tag is an anchor
if tag == "a":
# find href attribute
for attr in attrs:
if attr[0] == "href":
self.urls.append(attr[1])
+354 -57
View File
@@ -1,93 +1,157 @@
import copy
import itertools
import os
import stat
from contextlib import contextmanager
from json import JSONDecodeError
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Mapping
from typing import Dict, Iterator, List, Optional
from .dependencies import clean_resolved_dep, pep423_name, translate_markers
from pipenv.patched.pip._internal.req.req_install import InstallRequirement
from pipenv.utils.dependencies import (
clean_resolved_dep,
determine_vcs_revision_hash,
expansive_install_req_from_line,
pep423_name,
translate_markers,
)
from pipenv.utils.exceptions import (
LockfileCorruptException,
MissingParameter,
PipfileNotFound,
)
from pipenv.utils.pipfile import DEFAULT_NEWLINES, ProjectFile
from pipenv.utils.requirements import normalize_name
from pipenv.utils.requirementslib import is_editable, is_vcs, merge_items
from pipenv.vendor.plette import lockfiles
from pipenv.vendor.pydantic import BaseModel, Field
def format_requirement_for_lockfile(req, markers_lookup, index_lookup, hashes=None):
if req.specifiers:
version = str(req.get_version)
def merge_markers(entry, markers):
if not isinstance(markers, list):
markers = [markers]
for marker in markers:
if not isinstance(marker, str):
marker = str(marker)
if "markers" not in entry:
entry["markers"] = marker
elif marker not in entry["markers"]:
entry["markers"] = f"({entry['markers']}) and ({marker})"
def format_requirement_for_lockfile(
req: InstallRequirement,
markers_lookup,
index_lookup,
original_deps,
pipfile_entries,
hashes=None,
):
if req.specifier:
version = str(req.specifier)
else:
version = None
index = index_lookup.get(req.normalized_name)
markers = markers_lookup.get(req.normalized_name)
name = normalize_name(req.name)
index = index_lookup.get(name)
markers = req.markers
req.index = index
name, pf_entry = req.pipfile_entry
name = pep423_name(req.name)
pipfile_entry = pipfile_entries[name] if name in pipfile_entries else {}
entry = {}
if isinstance(pf_entry, str):
entry["version"] = pf_entry.lstrip("=")
else:
entry.update(pf_entry)
if version is not None and not req.is_vcs:
entry["version"] = version
if req.line_instance.is_direct_url and not req.is_vcs:
entry["file"] = req.req.uri
if req.link and req.link.is_vcs:
vcs = req.link.scheme.split("+", 1)[0]
entry["ref"] = determine_vcs_revision_hash(req, vcs, pipfile_entry.get("ref"))
if name in original_deps:
entry[vcs] = original_deps[name]
else:
entry[vcs] = req.link.url
if req.req:
entry["version"] = str(req.specifier)
elif version:
entry["version"] = version
elif req.link and req.link.is_file:
entry["file"] = req.link.url
if hashes:
entry["hashes"] = sorted(set(hashes))
entry["name"] = name
if index:
entry.update({"index": index})
if markers:
entry.update({"markers": markers})
entry.update({"markers": str(markers)})
if name in markers_lookup:
merge_markers(entry, markers_lookup[name])
if isinstance(pipfile_entry, dict) and "markers" in pipfile_entry:
merge_markers(entry, pipfile_entry["markers"])
if isinstance(pipfile_entry, dict) and "os_name" in pipfile_entry:
merge_markers(entry, f"os_name {pipfile_entry['os_name']}")
entry = translate_markers(entry)
if req.vcs or req.editable:
for key in ("index", "version", "file"):
try:
del entry[key]
except KeyError:
pass
if req.extras:
entry["extras"] = sorted(req.extras)
if isinstance(pipfile_entry, dict) and pipfile_entry.get("file"):
entry["file"] = pipfile_entry["file"]
entry["editable"] = True
entry.pop("version", None)
entry.pop("index", None)
elif isinstance(pipfile_entry, dict) and pipfile_entry.get("path"):
entry["path"] = pipfile_entry["path"]
entry["editable"] = True
entry.pop("version", None)
entry.pop("index", None)
return name, entry
def get_locked_dep(dep, pipfile_section):
entry = None
cleaner_kwargs = {"is_top_level": False, "pipfile_entry": None}
if isinstance(dep, Mapping) and dep.get("name"):
def get_locked_dep(project, dep, pipfile_section, current_entry=None):
# initialize default values
is_top_level = False
# if the dependency has a name, find corresponding entry in pipfile
if isinstance(dep, dict) and dep.get("name"):
dep_name = pep423_name(dep["name"])
for pipfile_key, pipfile_entry in pipfile_section.items():
if pep423_name(pipfile_key) == dep_name:
entry = pipfile_entry
if pep423_name(pipfile_key) == dep_name or pipfile_key == dep_name:
is_top_level = True
if isinstance(pipfile_entry, dict):
if pipfile_entry.get("version"):
pipfile_entry.pop("version")
if pipfile_entry.get("ref"):
pipfile_entry.pop("ref")
dep.update(pipfile_entry)
break
if entry:
cleaner_kwargs.update({"is_top_level": True, "pipfile_entry": entry})
lockfile_entry = clean_resolved_dep(dep, **cleaner_kwargs)
if entry and isinstance(entry, Mapping):
version = entry.get("version", "") if entry else ""
else:
version = entry if entry else ""
# clean the dependency
lockfile_entry = clean_resolved_dep(project, dep, is_top_level, current_entry)
# get the lockfile version and compare with pipfile version
lockfile_name, lockfile_dict = lockfile_entry.copy().popitem()
lockfile_version = lockfile_dict.get("version", "")
# Keep pins from the lockfile
if lockfile_version != version and version.startswith("==") and "*" not in version:
lockfile_dict["version"] = version
lockfile_entry[lockfile_name] = lockfile_dict
return lockfile_entry
def prepare_lockfile(results, pipfile, lockfile):
# from .vendor.requirementslib.utils import is_vcs
def prepare_lockfile(project, results, pipfile, lockfile_section, old_lock_data=None):
for dep in results:
if not dep:
continue
# Merge in any relevant information from the pipfile entry, including
# markers, normalized names, URL info, etc that we may have dropped during lock
# if not is_vcs(dep):
lockfile_entry = get_locked_dep(dep, pipfile)
name = next(iter(k for k in lockfile_entry.keys()))
current_entry = lockfile.get(name)
if current_entry:
if not isinstance(current_entry, Mapping):
lockfile[name] = lockfile_entry[name]
else:
lockfile[name].update(lockfile_entry[name])
lockfile[name] = translate_markers(lockfile[name])
dep_name = dep["name"]
current_entry = None
if dep_name in old_lock_data:
current_entry = old_lock_data[dep_name]
lockfile_entry = get_locked_dep(project, dep, pipfile, current_entry)
# If the current dependency doesn't exist in the lockfile, add it
if dep_name not in lockfile_section:
lockfile_section[dep_name] = lockfile_entry[dep_name]
else:
lockfile[name] = lockfile_entry[name]
return lockfile
# If the dependency exists, update the details
current_entry = lockfile_section[dep_name]
if not isinstance(current_entry, dict):
lockfile_section[dep_name] = lockfile_entry[dep_name]
else:
# If the current entry is a dict, merge the new details
lockfile_section[dep_name].update(lockfile_entry[dep_name])
lockfile_section[dep_name] = translate_markers(lockfile_section[dep_name])
return lockfile_section
@contextmanager
@@ -170,3 +234,236 @@ def atomic_open_for_write(target, binary=False, newline=None, encoding=None) ->
except OSError:
pass
os.rename(f.name, target) # No os.replace() on Python 2.
class Lockfile(BaseModel):
path: Path = Field(
default_factory=lambda: Path(os.curdir).joinpath("Pipfile.lock").absolute()
)
_requirements: Optional[list] = Field(default_factory=list)
_dev_requirements: Optional[list] = Field(default_factory=list)
projectfile: ProjectFile = None
lockfile: lockfiles.Lockfile
newlines: str = DEFAULT_NEWLINES
class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)
@property
def section_keys(self):
return set(self.lockfile.keys()) - {"_meta"}
@property
def extended_keys(self):
return [k for k in itertools.product(self.section_keys, ["", "vcs", "editable"])]
def get(self, k):
return self.__getitem__(k)
def __contains__(self, k):
check_lockfile = k in self.extended_keys or self.lockfile.__contains__(k)
if check_lockfile:
return True
return super().__contains__(k)
def __setitem__(self, k, v):
lockfile = self.lockfile
lockfile.__setitem__(k, v)
def __getitem__(self, k, *args, **kwargs):
retval = None
lockfile = self.lockfile
try:
retval = lockfile[k]
except KeyError:
if "-" in k:
section, _, pkg_type = k.rpartition("-")
vals = getattr(lockfile.get(section, {}), "_data", {})
if pkg_type == "vcs":
retval = {k: v for k, v in vals.items() if is_vcs(v)}
elif pkg_type == "editable":
retval = {k: v for k, v in vals.items() if is_editable(v)}
if retval is None:
raise
else:
retval = getattr(retval, "_data", retval)
return retval
def __getattr__(self, k, *args, **kwargs):
lockfile = self.lockfile
try:
return super().__getattribute__(k)
except AttributeError:
retval = getattr(lockfile, k, None)
if retval is not None:
return retval
return super().__getattribute__(k, *args, **kwargs)
def get_deps(self, dev=False, only=True):
deps = {}
if dev:
deps.update(self.develop._data)
if only:
return deps
deps = merge_items([deps, self.default._data])
return deps
@classmethod
def read_projectfile(cls, path):
pf = ProjectFile.read(path, lockfiles.Lockfile, invalid_ok=True)
return pf
@classmethod
def lockfile_from_pipfile(cls, pipfile_path):
from pipenv.utils.pipfile import Pipfile
if os.path.isfile(pipfile_path):
if not os.path.isabs(pipfile_path):
pipfile_path = os.path.abspath(pipfile_path)
pipfile = Pipfile.load(os.path.dirname(pipfile_path))
return lockfiles.Lockfile.with_meta_from(pipfile.pipfile)
raise PipfileNotFound(pipfile_path)
@classmethod
def load_projectfile(
cls, path: Optional[str] = None, create: bool = True, data: Optional[Dict] = None
) -> "ProjectFile":
if not path:
path = os.curdir
path = Path(path).absolute()
project_path = path if path.is_dir() else path.parent
lockfile_path = path if path.is_file() else project_path / "Pipfile.lock"
if not project_path.exists():
raise OSError(f"Project does not exist: {project_path.as_posix()}")
elif not lockfile_path.exists() and not create:
raise FileNotFoundError(
f"Lockfile does not exist: {lockfile_path.as_posix()}"
)
projectfile = cls.read_projectfile(lockfile_path.as_posix())
if not lockfile_path.exists():
if not data:
pipfile = project_path.joinpath("Pipfile")
lf = cls.lockfile_from_pipfile(pipfile)
else:
lf = lockfiles.Lockfile(data)
projectfile.model = lf
else:
if data:
raise ValueError("Cannot pass data when loading existing lockfile")
with open(lockfile_path.as_posix()) as f:
projectfile.model = lockfiles.Lockfile.load(f)
return projectfile
@classmethod
def from_data(
cls, path: Optional[str], data: Optional[Dict], meta_from_project: bool = True
) -> "Lockfile":
if path is None:
raise MissingParameter("path")
if data is None:
raise MissingParameter("data")
if not isinstance(data, dict):
raise TypeError("Expecting a dictionary for parameter 'data'")
path = os.path.abspath(str(path))
if os.path.isdir(path):
project_path = path
elif not os.path.isdir(path) and os.path.isdir(os.path.dirname(path)):
project_path = os.path.dirname(path)
pipfile_path = os.path.join(project_path, "Pipfile")
lockfile_path = os.path.join(project_path, "Pipfile.lock")
if meta_from_project:
lockfile = cls.lockfile_from_pipfile(pipfile_path)
lockfile.update(data)
else:
lockfile = lockfiles.Lockfile(data)
projectfile = ProjectFile(
line_ending=DEFAULT_NEWLINES, location=lockfile_path, model=lockfile
)
return cls(
projectfile=projectfile,
lockfile=lockfile,
newlines=projectfile.line_ending,
path=Path(projectfile.location),
)
@classmethod
def load(cls, path: Optional[str], create: bool = True) -> "Lockfile":
try:
projectfile = cls.load_projectfile(path, create=create)
except JSONDecodeError:
path = os.path.abspath(path)
path = Path(
os.path.join(path, "Pipfile.lock") if os.path.isdir(path) else path
)
formatted_path = path.as_posix()
backup_path = f"{formatted_path}.bak"
LockfileCorruptException.show(formatted_path, backup_path=backup_path)
path.rename(backup_path)
cls.load(formatted_path, create=True)
lockfile_path = Path(projectfile.location)
creation_args = {
"projectfile": projectfile,
"lockfile": projectfile.model,
"newlines": projectfile.line_ending,
"path": lockfile_path,
}
return cls(**creation_args)
@classmethod
def create(cls, path: Optional[str], create: bool = True) -> "Lockfile":
return cls.load(path, create=create)
def get_section(self, name: str) -> Optional[Dict]:
return self.lockfile.get(name)
@property
def develop(self) -> Dict:
return self.lockfile.develop
@property
def default(self) -> Dict:
return self.lockfile.default
def get_requirements(
self, dev: bool = True, only: bool = False, categories: Optional[List[str]] = None
) -> Iterator[InstallRequirement]:
from pipenv.utils.requirements import requirement_from_lockfile
if categories:
deps = {}
for category in categories:
if category == "packages":
category = "default"
elif category == "dev-packages":
category = "develop"
try:
category_deps = self[category]
except KeyError:
category_deps = {}
self.lockfile[category] = category_deps
deps = merge_items([deps, category_deps])
else:
deps = self.get_deps(dev=dev, only=only)
for package_name, package_info in deps.items():
pip_line = requirement_from_lockfile(
package_name, package_info, include_hashes=False, include_markers=False
)
pip_line_specified = requirement_from_lockfile(
package_name, package_info, include_hashes=True, include_markers=True
)
yield expansive_install_req_from_line(pip_line), pip_line_specified
def requirements_list(self, category: str) -> List[Dict]:
if self.lockfile.get(category):
return [
{name: entry._data} for name, entry in self.lockfile[category].items()
]
return []
def write(self) -> None:
self.projectfile.model = copy.deepcopy(self.lockfile)
self.projectfile.write()
@@ -3,24 +3,23 @@ import operator
import re
from collections.abc import Mapping, Set
from functools import reduce
from typing import Any, AnyStr, Iterator, List, Optional, Tuple, Type, Union
from typing import Optional
from pipenv.patched.pip._vendor.distlib import markers
from pipenv.patched.pip._vendor.packaging.markers import InvalidMarker, Marker
from pipenv.patched.pip._vendor.packaging.specifiers import LegacySpecifier, Specifier, SpecifierSet
from pipenv.patched.pip._vendor.packaging.specifiers import (
LegacySpecifier,
Specifier,
SpecifierSet,
)
from pipenv.vendor.pydantic import BaseModel
from ..exceptions import RequirementError
MAX_VERSIONS = {1: 7, 2: 7, 3: 11, 4: 0}
DEPRECATED_VERSIONS = ["3.0", "3.1", "3.2", "3.3"]
def is_instance(item, cls):
# type: (Any, Type) -> bool
if isinstance(item, cls) or item.__class__.__name__ == cls.__name__:
return True
return False
class RequirementError(Exception):
pass
class PipenvMarkers(BaseModel):
@@ -38,9 +37,7 @@ class PipenvMarkers(BaseModel):
@property
def line_part(self):
return " and ".join(
["{0} {1}".format(k, v) for k, v in self.dict().items() if v is not None]
)
return " and ".join([f"{k} {v}" for k, v in self.dict().items() if v is not None])
@property
def pipfile_part(self):
@@ -67,7 +64,7 @@ class PipenvMarkers(BaseModel):
def from_pipfile(cls, name, pipfile):
attr_fields = [field_name for field_name in cls.__fields__]
found_keys = [k for k in pipfile.keys() if k in attr_fields]
marker_strings = ["{0} {1}".format(k, pipfile[k]) for k in found_keys]
marker_strings = [f"{k} {pipfile[k]}" for k in found_keys]
if pipfile.get("markers"):
marker_strings.append(pipfile.get("markers"))
markers = set()
@@ -82,6 +79,13 @@ class PipenvMarkers(BaseModel):
return combined_marker
def is_instance(item, cls):
# type: (Any, Type) -> bool
if isinstance(item, cls) or item.__class__.__name__ == cls.__name__:
return True
return False
def _tuplize_version(version):
# type: (str) -> Union[Tuple[()], Tuple[int, ...], Tuple[int, int, str]]
output = []
@@ -110,7 +114,7 @@ def _format_pyspec(specifier):
# type: (Union[str, Specifier]) -> Specifier
if isinstance(specifier, str):
if not specifier.startswith(tuple(Specifier._operators.keys())):
specifier = "=={0}".format(specifier)
specifier = f"=={specifier}"
specifier = Specifier(specifier)
version = getattr(specifier, "version", specifier).rstrip()
if version:
@@ -121,7 +125,7 @@ def _format_pyspec(specifier):
if version.endswith(".*"):
version = version[:-2]
version = version.rstrip("*")
specifier = Specifier("{0}{1}".format(specifier.operator, version))
specifier = Specifier(f"{specifier.operator}{version}")
try:
op = REPLACE_RANGES[specifier.operator]
except KeyError:
@@ -137,7 +141,7 @@ def _format_pyspec(specifier):
next_tuple = (next_tuple[0], curr_tuple[1])
else:
return specifier
specifier = Specifier("{0}{1}".format(op, _format_version(next_tuple)))
specifier = Specifier(f"{op}{_format_version(next_tuple)}")
return specifier
@@ -214,9 +218,7 @@ def normalize_specifier_set(specs):
# And rename it to something meaningful
def get_sorted_version_string(version_set):
# type: (Set[AnyStr]) -> AnyStr
version_list = sorted(
"{0}".format(_format_version(version)) for version in version_set
)
version_list = sorted(f"{_format_version(version)}" for version in version_set)
version = ", ".join(version_list)
return version
@@ -495,7 +497,7 @@ def _split_specifierset_str(specset_str, prefix="=="):
if prefix == "!=" and any(v in values for v in DEPRECATED_VERSIONS):
values += DEPRECATED_VERSIONS[:]
for value in sorted(values):
specifiers.add(Specifier("{0}{1}".format(prefix, value)))
specifiers.add(Specifier(f"{prefix}{value}"))
return specifiers
@@ -516,7 +518,7 @@ def _get_specifiers_from_markers(marker_item):
elif op.value == "not in":
specifiers.update(_split_specifierset_str(value.value, prefix="!="))
else:
specifiers.add(Specifier("{0}{1}".format(op.value, value.value)))
specifiers.add(Specifier(f"{op.value}{value.value}"))
elif isinstance(marker_item, list):
parts = get_specset(marker_item)
if parts:
@@ -611,12 +613,17 @@ def _contains_micro_version(version_string):
return re.search(r"\d+\.\d+\.\d+", version_string) is not None
def format_pyversion(parts):
op, val = parts
version_marker = (
"python_full_version" if _contains_micro_version(val) else "python_version"
)
return "{0} {1} '{2}'".format(version_marker, op, val)
def merge_markers(m1, m2):
# type: (Marker, Marker) -> Optional[Marker]
if not all((m1, m2)):
return next(iter(v for v in (m1, m2) if v), None)
m1 = _ensure_marker(m1)
m2 = _ensure_marker(m2)
_markers = [] # type: List[Marker]
for marker in (m1, m2):
_markers.append(str(marker))
marker_str = " and ".join([normalize_marker_str(m) for m in _markers if m])
return _ensure_marker(normalize_marker_str(marker_str))
def normalize_marker_str(marker) -> str:
@@ -632,9 +639,9 @@ def normalize_marker_str(marker) -> str:
marker_str = " and ".join([format_pyversion(pv) for pv in parts])
if marker:
if marker_str:
marker_str = "{0!s} and {1!s}".format(marker_str, marker)
marker_str = f"{marker_str!s} and {marker!s}"
else:
marker_str = "{0!s}".format(marker)
marker_str = f"{marker!s}"
return marker_str.replace('"', "'")
@@ -642,9 +649,9 @@ def marker_from_specifier(spec) -> Marker:
if not any(spec.startswith(k) for k in Specifier._operators.keys()):
if spec.strip().lower() in ["any", "<any>", "*"]:
return None
spec = "=={0}".format(spec)
spec = f"=={spec}"
elif spec.startswith("==") and spec.count("=") > 3:
spec = "=={0}".format(spec.lstrip("="))
spec = "=={}".format(spec.lstrip("="))
if not spec:
return None
marker_segments = []
@@ -654,14 +661,9 @@ def marker_from_specifier(spec) -> Marker:
return Marker(marker_str)
def merge_markers(m1, m2):
# type: (Marker, Marker) -> Optional[Marker]
if not all((m1, m2)):
return next(iter(v for v in (m1, m2) if v), None)
m1 = _ensure_marker(m1)
m2 = _ensure_marker(m2)
_markers = [] # type: List[Marker]
for marker in (m1, m2):
_markers.append(str(marker))
marker_str = " and ".join([normalize_marker_str(m) for m in _markers if m])
return _ensure_marker(normalize_marker_str(marker_str))
def format_pyversion(parts):
op, val = parts
version_marker = (
"python_full_version" if _contains_micro_version(val) else "python_version"
)
return f"{version_marker} {op} '{val}'"
+19 -259
View File
@@ -1,72 +1,14 @@
import logging
import os
import tempfile
from pathlib import Path
from typing import List, Optional
from pipenv.patched.pip._internal.build_env import get_runnable_pip
from pipenv.project import Project
from pipenv.utils import err
from pipenv.utils.dependencies import get_constraints_from_deps, prepare_constraint_file
from pipenv.utils.indexes import get_source_list, prepare_pip_source_args
from pipenv.utils.fileutils import create_tracked_tempdir, normalize_path
from pipenv.utils.indexes import prepare_pip_source_args
from pipenv.utils.processes import subprocess_run
from pipenv.utils.shell import cmd_list_to_shell, project_python
from pipenv.vendor.requirementslib import Requirement
from pipenv.vendor.requirementslib.fileutils import create_tracked_tempdir, normalize_path
def format_pip_output(out, r=None):
def gen(out):
for line in out.split("\n"):
# Remove requirements file information from pip9 output.
if "(from -r" in line:
yield line[: line.index("(from -r")]
else:
yield line
out = "\n".join([line for line in gen(out)])
return out
def format_pip_error(error):
error = error.replace("Expected", "[bold green]Expected[/bold green]")
error = error.replace("Got", "[bold red]Got[/red bold]")
error = error.replace(
"THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE",
"[bold red]THESE PACKAGES DO NOT MATCH THE HASHES FROM Pipfile.lock![/bold red]",
)
error = error.replace(
"someone may have tampered with them",
"[red]someone may have tampered with them[/red]",
)
error = error.replace("option to pip install", "option to 'pipenv install'")
return error
def pip_download(project, package_name):
cache_dir = Path(project.s.PIPENV_CACHE_DIR)
pip_config = {
"PIP_CACHE_DIR": cache_dir.as_posix(),
"PIP_WHEEL_DIR": cache_dir.joinpath("wheels").as_posix(),
"PIP_DESTINATION_DIR": cache_dir.joinpath("pkgs").as_posix(),
}
for source in project.sources:
cmd = [
project_python(project),
get_runnable_pip(),
"download",
package_name,
"-i",
source["url"],
"-d",
project.download_location,
]
c = subprocess_run(cmd, env=pip_config)
if c.returncode == 0:
break
return c
def pip_install_deps(
@@ -95,56 +37,29 @@ def pip_install_deps(
editable_requirements = tempfile.NamedTemporaryFile(
prefix="pipenv-", suffix="-reqs.txt", dir=requirements_dir, delete=False
)
for requirement in deps:
ignore_hash = ignore_hashes
vcs_or_editable = (
requirement.is_vcs
or requirement.vcs
or requirement.editable
or (requirement.is_file_or_url and not requirement.hashes)
)
if vcs_or_editable:
ignore_hash = True
if requirement and vcs_or_editable:
requirement.index = None
line = requirement.line_instance.get_line(
with_prefix=True,
with_hashes=not ignore_hash,
with_markers=True,
as_list=False,
)
for pip_line in deps:
ignore_hash = ignore_hashes or "--hash" not in pip_line
if project.s.is_verbose():
err.print(f"Writing supplied requirement line to temporary file: {line!r}")
target = editable_requirements if vcs_or_editable else standard_requirements
target.write(line.encode())
err.print(
f"Writing supplied requirement line to temporary file: {pip_line!r}"
)
target = editable_requirements if ignore_hash else standard_requirements
target.write(pip_line.encode())
target.write(b"\n")
standard_requirements.close()
editable_requirements.close()
cmds = []
files = []
standard_deps = list(
filter(
lambda d: not (
d.is_vcs or d.vcs or d.editable or (d.is_file_or_url and not d.hashes)
),
deps,
)
)
if standard_deps:
files.append(standard_requirements)
editable_deps = list(
filter(
lambda d: d.is_vcs
or d.vcs
or d.editable
or (d.is_file_or_url and not d.hashes),
deps,
)
)
if editable_deps:
files.append(editable_requirements)
for pip_line in deps:
if "--hash" in pip_line and standard_requirements not in files:
files.append(standard_requirements)
elif editable_requirements not in files:
files.append(editable_requirements)
for file in files:
pip_command = [
project_python(project, system=allow_global),
@@ -166,10 +81,8 @@ def pip_install_deps(
if project.s.is_verbose():
msg = f"Install Phase: {'Standard Requirements' if file == standard_requirements else 'Editable Requirements'}"
err.print(msg, style="bold")
for requirement in (
standard_deps if file == standard_requirements else editable_deps
):
err.print(f"Preparing Installation of {requirement.name!r}", style="bold")
for pip_line in deps:
err.print(f"Preparing Installation of {pip_line!r}", style="bold")
err.print(f"$ {cmd_list_to_shell(pip_command)}", style="cyan")
cache_dir = Path(project.s.PIPENV_CACHE_DIR)
default_exists_action = "w"
@@ -186,10 +99,6 @@ def pip_install_deps(
err.print(f"Using source directory: {src_dir!r}")
pip_config.update({"PIP_SRC": src_dir})
c = subprocess_run(pip_command, block=False, capture_output=True, env=pip_config)
if file == standard_requirements:
c.deps = standard_deps
else:
c.deps = editable_deps
c.env = pip_config
cmds.append(c)
if project.s.is_verbose():
@@ -204,132 +113,6 @@ def pip_install_deps(
return cmds
def pip_install(
project,
requirement=None,
r=None,
allow_global=False,
ignore_hashes=False,
no_deps=False,
block=True,
index=None,
pre=False,
dev=False,
requirements_dir=None,
extra_indexes=None,
pypi_mirror=None,
use_pep517=True,
use_constraint=False,
extra_pip_args: Optional[List] = None,
):
piplogger = logging.getLogger("pipenv.patched.pip._internal.commands.install")
trusted_hosts = get_trusted_hosts()
if not allow_global:
src_dir = os.getenv(
"PIP_SRC", os.getenv("PIP_SRC_DIR", project.virtualenv_src_location)
)
else:
src_dir = os.getenv("PIP_SRC", os.getenv("PIP_SRC_DIR"))
if requirement:
if requirement.editable or not requirement.hashes:
ignore_hashes = True
elif not (requirement.is_vcs or requirement.editable or requirement.vcs):
ignore_hashes = False
line = None
# Try installing for each source in project.sources.
search_all_sources = project.settings.get("install_search_all_sources", False)
if not index and requirement.index:
index = requirement.index
if index and not extra_indexes:
if search_all_sources:
extra_indexes = list(project.sources)
else: # Default: index restrictions apply during installation
extra_indexes = []
if requirement.index:
extra_indexes = list(
filter(lambda d: d.get("name") == requirement.index, project.sources)
)
if not extra_indexes:
extra_indexes = list(project.sources)
if requirement and requirement.vcs or requirement.editable:
requirement.index = None
r = write_requirement_to_file(
project,
requirement,
requirements_dir=requirements_dir,
include_hashes=not ignore_hashes,
)
sources = get_source_list(
project,
index,
extra_indexes=extra_indexes,
trusted_hosts=trusted_hosts,
pypi_mirror=pypi_mirror,
)
source_names = {src.get("name") for src in sources}
if not search_all_sources and requirement.index in source_names:
sources = list(filter(lambda d: d.get("name") == requirement.index, sources))
if r:
with open(r) as fh:
if "--hash" not in fh.read():
ignore_hashes = True
if project.s.is_verbose():
piplogger.setLevel(logging.WARN)
if requirement:
err.print(f"Installing {requirement.name!r}", style="bold")
pip_command = [
project_python(project, system=allow_global),
get_runnable_pip(),
"install",
]
pip_args = get_pip_args(
project,
pre=pre,
verbose=project.s.is_verbose(),
upgrade=True,
no_use_pep517=not use_pep517,
no_deps=no_deps,
require_hashes=not ignore_hashes,
extra_pip_args=extra_pip_args,
)
pip_command.extend(pip_args)
if r:
pip_command.extend(["-r", normalize_path(r)])
elif line:
pip_command.extend(line)
if dev and use_constraint:
default_constraints = get_constraints_from_deps(project.packages)
constraint_filename = prepare_constraint_file(
default_constraints,
directory=requirements_dir,
sources=None,
pip_args=None,
)
pip_command.extend(["-c", normalize_path(constraint_filename)])
pip_command.extend(prepare_pip_source_args(sources))
if project.s.is_verbose():
err.print(f"$ {cmd_list_to_shell(pip_command)}")
cache_dir = Path(project.s.PIPENV_CACHE_DIR)
default_exists_action = "w"
exists_action = project.s.PIP_EXISTS_ACTION or default_exists_action
pip_config = {
"PIP_CACHE_DIR": cache_dir.as_posix(),
"PIP_WHEEL_DIR": cache_dir.joinpath("wheels").as_posix(),
"PIP_DESTINATION_DIR": cache_dir.joinpath("pkgs").as_posix(),
"PIP_EXISTS_ACTION": exists_action,
"PATH": os.environ.get("PATH"),
}
if src_dir:
if project.s.is_verbose():
err(f"Using source directory: {src_dir!r}")
pip_config.update({"PIP_SRC": src_dir})
c = subprocess_run(pip_command, block=block, env=pip_config)
c.env = pip_config
return c
def get_pip_args(
project,
pre: bool = False,
@@ -366,26 +149,3 @@ def get_trusted_hosts():
return os.environ.get("PIP_TRUSTED_HOSTS", []).split(" ")
except AttributeError:
return []
def write_requirement_to_file(
project: Project,
requirement: Requirement,
requirements_dir: Optional[str] = None,
include_hashes: bool = True,
) -> str:
if not requirements_dir:
requirements_dir = create_tracked_tempdir(prefix="pipenv", suffix="requirements")
line = requirement.line_instance.get_line(
with_prefix=True, with_hashes=include_hashes, with_markers=True, as_list=False
)
f = tempfile.NamedTemporaryFile(
prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir, delete=False
)
if project.s.is_verbose():
err.print(f"Writing supplied requirement line to temporary file: {line!r}")
f.write(line.encode())
r = f.name
f.close()
return r
+320
View File
@@ -1,8 +1,21 @@
import io
import itertools
import os
from pathlib import Path
from typing import Any, Dict, List, Optional
from pipenv import environments, exceptions
from pipenv.utils import console, err
from pipenv.utils.internet import get_url_name
from pipenv.utils.markers import RequirementError
from pipenv.utils.requirements import import_requirements
from pipenv.utils.requirementslib import is_editable, is_vcs, merge_items
from pipenv.utils.toml import tomlkit_value_to_python
from pipenv.vendor import tomlkit
from pipenv.vendor.plette import pipfiles
from pipenv.vendor.pydantic import BaseModel, Field, validator
DEFAULT_NEWLINES = "\n"
def walk_up(bottom):
@@ -110,3 +123,310 @@ def ensure_pipfile(
if changed:
err.print("Fixing package names in Pipfile...", style="bold")
project.write_toml(p)
def reorder_source_keys(data):
# type: (tomlkit.toml_document.TOMLDocument) -> tomlkit.toml_document.TOMLDocument
sources = [] # type: sources_type
for source_key in ["source", "sources"]:
sources.extend(data.get(source_key, tomlkit.aot()).value)
new_source_aot = tomlkit.aot()
for entry in sources:
table = tomlkit.table() # type: tomlkit.items.Table
source_entry = PipfileLoader.populate_source(entry.copy())
for key in ["name", "url", "verify_ssl"]:
table.update({key: source_entry[key]})
new_source_aot.append(table)
data["source"] = new_source_aot
if data.get("sources", None):
del data["sources"]
return data
def preferred_newlines(f):
if isinstance(f.newlines, str):
return f.newlines
return DEFAULT_NEWLINES
class ProjectFile(BaseModel):
location: str
line_ending: str
model: Optional[Any] = Field(default_factory=lambda: {})
@classmethod
def read(cls, location: str, model_cls, invalid_ok: bool = False) -> "ProjectFile":
if not os.path.exists(location) and not invalid_ok:
raise FileNotFoundError(location)
try:
with open(location, encoding="utf-8") as f:
model = model_cls.load(f)
line_ending = preferred_newlines(f)
except Exception:
if not invalid_ok:
raise
model = {}
line_ending = DEFAULT_NEWLINES
return cls(location=location, line_ending=line_ending, model=model)
def write(self) -> None:
kwargs = {"encoding": "utf-8", "newline": self.line_ending}
with open(self.location, "w", **kwargs) as f:
if self.model:
self.model.dump(f)
def dumps(self) -> str:
if self.model:
strio = io.StringIO()
self.model.dump(strio)
return strio.getvalue()
return ""
class PipfileLoader(pipfiles.Pipfile):
@classmethod
def validate(cls, data):
# type: (tomlkit.toml_document.TOMLDocument) -> None
for key, klass in pipfiles.PIPFILE_SECTIONS.items():
if key not in data or key == "sources":
continue
try:
klass.validate(data[key])
except Exception:
pass
@classmethod
def ensure_package_sections(cls, data):
# type: (tomlkit.toml_document.TOMLDocument[Text, Any]) -> tomlkit.toml_document.TOMLDocument[Text, Any]
"""Ensure that all pipfile package sections are present in the given
toml document.
:param :class:`~tomlkit.toml_document.TOMLDocument` data: The toml document to
ensure package sections are present on
:return: The updated toml document, ensuring ``packages`` and ``dev-packages``
sections are present
:rtype: :class:`~tomlkit.toml_document.TOMLDocument`
"""
package_keys = (
k for k in pipfiles.PIPFILE_SECTIONS.keys() if k.endswith("packages")
)
for key in package_keys:
if key not in data:
data.update({key: tomlkit.table()})
return data
@classmethod
def populate_source(cls, source):
"""Derive missing values of source from the existing fields."""
# Only URL pararemter is mandatory, let the KeyError be thrown.
if "name" not in source:
source["name"] = get_url_name(source["url"])
if "verify_ssl" not in source:
source["verify_ssl"] = "https://" in source["url"]
if not isinstance(source["verify_ssl"], bool):
source["verify_ssl"] = str(source["verify_ssl"]).lower() == "true"
return source
@classmethod
def load(cls, f, encoding=None):
# type: (Any, Text) -> PipfileLoader
content = f.read()
if encoding is not None:
content = content.decode(encoding)
_data = tomlkit.loads(content)
should_reload = "source" not in _data
_data = reorder_source_keys(_data)
if should_reload:
if "sources" in _data:
content = tomlkit.dumps(_data)
else:
# HACK: There is no good way to prepend a section to an existing
# TOML document, but there's no good way to copy non-structural
# content from one TOML document to another either. Modify the
# TOML content directly, and load the new in-memory document.
sep = "" if content.startswith("\n") else "\n"
content = pipfiles.DEFAULT_SOURCE_TOML + sep + content
data = tomlkit.loads(content)
data = cls.ensure_package_sections(data)
instance = cls(data)
instance._data = dict(instance._data)
return instance
def __contains__(self, key):
# type: (Text) -> bool
if key not in self._data:
package_keys = self._data.get("packages", {}).keys()
dev_package_keys = self._data.get("dev-packages", {}).keys()
return any(key in pkg_list for pkg_list in (package_keys, dev_package_keys))
return True
def __getattribute__(self, key):
# type: (Text) -> Any
if key == "source":
return self._data[key]
return super().__getattribute__(key)
class Pipfile(BaseModel):
path: Path
projectfile: ProjectFile
pipfile: Optional[PipfileLoader]
_pyproject: Optional[tomlkit.TOMLDocument] = tomlkit.document()
build_system: Optional[Dict] = {}
_requirements: Optional[List] = []
_dev_requirements: Optional[List] = []
class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)
@validator("path", pre=True, always=True)
def _get_path(cls, v):
return v or Path(os.curdir).absolute()
@validator("projectfile", pre=True, always=True)
def _get_projectfile(cls, v, values):
return v or cls.load_projectfile(os.curdir, create=False)
@validator("pipfile", pre=True, always=True)
def _get_pipfile(cls, v, values):
return v or values["projectfile"].model
@property
def root(self):
return self.path.parent
@property
def extended_keys(self):
return [
k
for k in itertools.product(
("packages", "dev-packages"), ("", "vcs", "editable")
)
]
def get_deps(self, dev=False, only=True):
deps = {} # type: Dict[Text, Dict[Text, Union[List[Text], Text]]]
if dev:
deps.update(dict(self.pipfile._data.get("dev-packages", {})))
if only:
return deps
return tomlkit_value_to_python(
merge_items([deps, dict(self.pipfile._data.get("packages", {}))])
)
def get(self, k):
return self.__getitem__(k)
def __contains__(self, k):
check_pipfile = k in self.extended_keys or self.pipfile.__contains__(k)
if check_pipfile:
return True
return False
def __getitem__(self, k, *args, **kwargs):
retval = None
pipfile = self.pipfile
section = None
pkg_type = None
try:
retval = pipfile[k]
except KeyError:
if "-" in k:
section, _, pkg_type = k.rpartition("-")
vals = getattr(pipfile.get(section, {}), "_data", {})
vals = tomlkit_value_to_python(vals)
if pkg_type == "vcs":
retval = {k: v for k, v in vals.items() if is_vcs(v)}
elif pkg_type == "editable":
retval = {k: v for k, v in vals.items() if is_editable(v)}
if retval is None:
raise
else:
retval = getattr(retval, "_data", retval)
return retval
def __getattr__(self, k, *args, **kwargs):
pipfile = self.pipfile
try:
retval = super(Pipfile).__getattribute__(k)
except AttributeError:
retval = getattr(pipfile, k, None)
return retval
@property
def requires_python(self):
# type: () -> bool
return getattr(
self.pipfile.requires,
"python_version",
getattr(self.pipfile.requires, "python_full_version", None),
)
@property
def allow_prereleases(self):
# type: () -> bool
return self.pipfile.get("pipenv", {}).get("allow_prereleases", False)
@classmethod
def read_projectfile(cls, path):
# type: (Text) -> ProjectFile
"""Read the specified project file and provide an interface for
writing/updating.
:param Text path: Path to the target file.
:return: A project file with the model and location for interaction
:rtype: :class:`~project.ProjectFile`
"""
pf = ProjectFile.read(path, PipfileLoader, invalid_ok=True)
return pf
@classmethod
def load_projectfile(cls, path, create=False):
# type: (Text, bool) -> ProjectFile
"""Given a path, load or create the necessary pipfile.
:param Text path: Path to the project root or pipfile
:param bool create: Whether to create the pipfile if not found, defaults to True
:raises OSError: Thrown if the project root directory doesn't exist
:raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False``
:return: A project file instance for the supplied project
:rtype: :class:`~project.ProjectFile`
"""
if not path:
raise RuntimeError("Must pass a path to classmethod 'Pipfile.load'")
if not isinstance(path, Path):
path = Path(path).absolute()
pipfile_path = path if path.is_file() else path.joinpath("Pipfile")
project_path = pipfile_path.parent
if not project_path.exists():
raise FileNotFoundError("%s is not a valid project path!" % path)
elif not pipfile_path.exists() or not pipfile_path.is_file():
if not create:
raise RequirementError("%s is not a valid Pipfile" % pipfile_path)
return cls.read_projectfile(pipfile_path.as_posix())
@classmethod
def load(cls, path, create=False):
# type: (Text, bool) -> Pipfile
"""Given a path, load or create the necessary pipfile.
:param Text path: Path to the project root or pipfile
:param bool create: Whether to create the pipfile if not found, defaults to True
:raises OSError: Thrown if the project root directory doesn't exist
:raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False``
:return: A pipfile instance pointing at the supplied project
:rtype:: class:`~pipfile.Pipfile`
"""
projectfile = cls.load_projectfile(path, create=create)
pipfile = projectfile.model
creation_args = {
"projectfile": projectfile,
"pipfile": pipfile,
"path": Path(projectfile.location),
}
return cls(**creation_args)
+21
View File
@@ -1,6 +1,9 @@
import os
from functools import lru_cache
from pipenv import exceptions
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
from pipenv.patched.pip._vendor.pkg_resources import Requirement, get_distribution
from pipenv.utils.dependencies import python_version
from pipenv.utils.pipfile import ensure_pipfile
from pipenv.utils.shell import shorten_path
@@ -82,3 +85,21 @@ def ensure_project(
categories=categories,
)
os.environ["PIP_PYTHON_PATH"] = project.python(system=system)
@lru_cache()
def get_setuptools_version():
# type: () -> Optional[STRING_TYPE]
setuptools_dist = get_distribution(Requirement("setuptools"))
return getattr(setuptools_dist, "version", None)
def get_default_pyproject_backend():
# type: () -> STRING_TYPE
st_version = get_setuptools_version()
if st_version is not None:
parsed_st_version = parse_version(st_version)
if parsed_st_version >= parse_version("40.8.0"):
return "setuptools.build_meta:__legacy__"
return "setuptools.build_meta"
+137 -23
View File
@@ -1,17 +1,60 @@
import os
import re
import urllib.parse
from typing import Tuple
from pipenv.patched.pip._internal.network.session import PipSession
from pipenv.patched.pip._internal.req import parse_requirements
from pipenv.patched.pip._internal.req.constructors import (
install_req_from_parsed_requirement,
)
from pipenv.patched.pip._internal.utils.misc import split_auth_from_netloc
from pipenv.patched.pip._internal.utils.misc import _transform_url, split_auth_from_netloc
from pipenv.utils.constants import VCS_LIST
from pipenv.utils.indexes import parse_indexes
from pipenv.utils.internet import get_host_and_port
from pipenv.utils.pip import get_trusted_hosts
def redact_netloc(netloc: str) -> Tuple[str]:
"""
Replace the sensitive data in a netloc with "****", if it exists, unless it's an environment variable.
For example:
- "user:pass@example.com" returns "user:****@example.com"
- "accesstoken@example.com" returns "****@example.com"
- "${ENV_VAR}:pass@example.com" returns "${ENV_VAR}:****@example.com" if ${ENV_VAR} is an environment variable
"""
netloc, (user, password) = split_auth_from_netloc(netloc)
if user is None:
return (netloc,)
if password is None:
# Check if user is an environment variable
if not re.match(r"\$\{\w+\}", user):
# If not, redact the user
user = "****"
password = ""
else:
# Check if password is an environment variable
if not re.match(r"\$\{\w+\}", password):
# If not, redact the password
password = ":****"
else:
# If it is, leave it as is
password = ":" + password
user = urllib.parse.quote(user)
return (f"{user}{password}@{netloc}",)
def redact_auth_from_url(url: str) -> str:
"""Replace the password in a given url with ****."""
return _transform_url(url, redact_netloc)[0]
def normalize_name(pkg) -> str:
"""Given a package name, return its normalized, non-canonicalized form."""
return pkg.replace("_", "-").lower()
def import_requirements(project, r=None, dev=False, categories=None):
# Parse requirements.txt file with Pip's parser.
# Pip requires a `PipSession` which is a subclass of requests.Session.
@@ -36,44 +79,34 @@ def import_requirements(project, r=None, dev=False, categories=None):
indexes.append(extra_index)
if trusted_host:
trusted_hosts.append(get_host_and_port(trusted_host))
indexes = sorted(set(indexes))
trusted_hosts = sorted(set(trusted_hosts))
reqs = [
install_req_from_parsed_requirement(f)
for f in parse_requirements(r, session=PipSession())
]
for package in reqs:
for f in parse_requirements(r, session=PipSession()):
package = install_req_from_parsed_requirement(f)
if package.name not in BAD_PACKAGES:
if package.link is not None:
if package.editable:
package_string = f"-e {package.link}"
else:
netloc, (user, pw) = split_auth_from_netloc(package.link.netloc)
safe = True
if user and not re.match(r"\${[\W\w]+}", user):
safe = False
if pw and not re.match(r"\${[\W\w]+}", pw):
safe = False
if safe:
package_string = str(package.link._url)
else:
package_string = str(package.link)
package_string = urllib.parse.unquote(
redact_auth_from_url(package.original_link.url)
)
if categories:
for category in categories:
project.add_package_to_pipfile(
package_string, dev=dev, category=category
package, package_string, dev=dev, category=category
)
else:
project.add_package_to_pipfile(package_string, dev=dev)
project.add_package_to_pipfile(package, package_string, dev=dev)
else:
if categories:
for category in categories:
project.add_package_to_pipfile(
str(package.req), dev=dev, category=category
package, str(package.req), dev=dev, category=category
)
else:
project.add_package_to_pipfile(str(package.req), dev=dev)
project.add_package_to_pipfile(package, str(package.req), dev=dev)
indexes = sorted(set(indexes))
trusted_hosts = sorted(set(trusted_hosts))
for index in indexes:
add_index_to_pipfile(project, index, trusted_hosts)
project.recase_pipfile()
@@ -105,3 +138,84 @@ BAD_PACKAGES = (
"setuptools",
"wheel",
)
def requirement_from_lockfile(
package_name, package_info, include_hashes=True, include_markers=True
):
from pipenv.utils.dependencies import is_editable_path, is_star
# Handle string requirements
if isinstance(package_info, str):
if package_info and not is_star(package_info):
return f"{package_name}=={package_info}"
else:
return package_name
# Handling vcs repositories
for vcs in VCS_LIST:
if vcs in package_info:
url = package_info[vcs]
ref = package_info.get("ref", "")
extras = (
"[{}]".format(",".join(package_info.get("extras", [])))
if "extras" in package_info
else ""
)
include_vcs = "" if f"{vcs}+" in url else f"{vcs}+"
egg_fragment = "" if "#egg=" in url else f"#egg={package_name}"
ref_str = "" if f"@{ref}" in url else f"@{ref}"
if is_editable_path(url) or "file://" in url:
pip_line = f"-e {include_vcs}{url}{ref_str}{egg_fragment}{extras}"
else:
pip_line = f"{package_name}{extras}@ {include_vcs}{url}{ref_str}"
return pip_line
# Handling file-sourced packages
for k in ["file", "path"]:
line = []
if k in package_info:
path = package_info[k]
if is_editable_path(path):
line.append("-e")
line.append(f"{package_info[k]}")
pip_line = " ".join(line)
return pip_line
# Handling packages from standard pypi like indexes
version = package_info.get("version", "").replace("==", "")
hashes = (
f" --hash={' --hash='.join(package_info['hashes'])}"
if include_hashes and "hashes" in package_info
else ""
)
markers = (
"; {}".format(package_info["markers"])
if include_markers and "markers" in package_info and package_info["markers"]
else ""
)
os_markers = (
"; {}".format(package_info["os_markers"])
if include_markers and "os_markers" in package_info and package_info["os_markers"]
else ""
)
extras = (
"[{}]".format(",".join(package_info.get("extras", [])))
if "extras" in package_info
else ""
)
pip_line = f"{package_name}{extras}=={version}{os_markers}{markers}{hashes}"
return pip_line
def requirements_from_lockfile(deps, include_hashes=True, include_markers=True):
pip_packages = []
for package_name, package_info in deps.items():
pip_package = requirement_from_lockfile(
package_name, package_info, include_hashes, include_markers
)
# Append to the list
pip_packages.append(pip_package)
# pip_packages contains the pip-installable lines
return pip_packages
@@ -1,77 +1,35 @@
# This Module is taken in part from the click project and expanded
# see https://github.com/pallets/click/blob/6cafd32/click/_winconsole.py
# Copyright © 2014 by the Pallets team.
# Some rights reserved.
# Redistribution and use in source and binary forms of the software as well as
# documentation, with or without modification, are permitted provided that the
# following conditions are met:
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
# THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND
# DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import os
import sys
from collections.abc import ItemsView, Mapping, Sequence, Set
from contextlib import contextmanager
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TypeVar, Union
from urllib.parse import urlparse, urlsplit, urlunparse
import pipenv.vendor.tomlkit as tomlkit
from pipenv.patched.pip._internal.commands.install import InstallCommand
from pipenv.patched.pip._internal.models.link import Link
from pipenv.patched.pip._internal.models.target_python import TargetPython
from pipenv.patched.pip._internal.network.download import Downloader
from pipenv.patched.pip._internal.operations.prepare import (
File,
_check_download_dir,
get_file_url,
unpack_vcs_link,
)
from pipenv.patched.pip._internal.utils.filetypes import is_archive_file
from pipenv.patched.pip._internal.utils.hashes import Hashes
from pipenv.patched.pip._internal.utils.misc import is_installable_dir
from pipenv.patched.pip._internal.utils.temp_dir import TempDirectory
from pipenv.patched.pip._internal.utils.unpacking import unpack_file
from pipenv.patched.pip._vendor.packaging import specifiers
from pipenv.utils.fileutils import is_valid_url, normalize_path, url_to_path
from pipenv.vendor import tomlkit
from .environment import MYPY_RUNNING
from .fileutils import is_valid_url, normalize_path, url_to_path
if MYPY_RUNNING:
from typing import Dict, Iterator, List, Optional, Text, Tuple, TypeVar, Union
STRING_TYPE = Union[bytes, str, Text]
S = TypeVar("S", bytes, str, Text)
PipfileEntryType = Union[STRING_TYPE, bool, Tuple[STRING_TYPE], List[STRING_TYPE]]
PipfileType = Union[STRING_TYPE, Dict[STRING_TYPE, PipfileEntryType]]
STRING_TYPE = Union[bytes, str, str]
S = TypeVar("S", bytes, str, str)
PipfileEntryType = Union[STRING_TYPE, bool, Tuple[STRING_TYPE], List[STRING_TYPE]]
PipfileType = Union[STRING_TYPE, Dict[STRING_TYPE, PipfileEntryType]]
VCS_LIST = ("git", "svn", "hg", "bzr")
def setup_logger():
logger = logging.getLogger("requirementslib")
loglevel = logging.DEBUG
handler = logging.StreamHandler(stream=sys.stderr)
handler.setLevel(loglevel)
logger.addHandler(handler)
logger.setLevel(loglevel)
return logger
log = setup_logger()
SCHEME_LIST = ("http://", "https://", "ftp://", "ftps://", "file://")
@@ -111,10 +69,8 @@ def strip_ssh_from_git_uri(uri):
# split the path on the first separating / so we can put the first segment
# into the 'netloc' section with a : separator
path_part, _, path = parsed.path.lstrip("/").partition("/")
path = "/{0}".format(path)
parsed = parsed._replace(
netloc="{0}:{1}".format(parsed.netloc, path_part), path=path
)
path = f"/{path}"
parsed = parsed._replace(netloc=f"{parsed.netloc}:{path_part}", path=path)
uri = urlunparse(parsed).replace("git+ssh://", "git+", 1)
return uri
@@ -129,7 +85,7 @@ def add_ssh_scheme_to_git_uri(uri):
parsed = urlparse(uri)
if ":" in parsed.netloc:
netloc, _, path_start = parsed.netloc.rpartition(":")
path = "/{0}{1}".format(path_start, parsed.path)
path = f"/{path_start}{parsed.path}"
uri = urlunparse(parsed._replace(netloc=netloc, path=path))
return uri
@@ -170,10 +126,10 @@ def convert_entry_to_path(path):
"""Convert a pipfile entry to a string."""
if not isinstance(path, Mapping):
raise TypeError("expecting a mapping, received {0!r}".format(path))
raise TypeError(f"expecting a mapping, received {path!r}")
if not any(key in path for key in ["file", "path"]):
raise ValueError("missing path-like entry in supplied mapping {0!r}".format(path))
raise ValueError(f"missing path-like entry in supplied mapping {path!r}")
if "file" in path:
path = url_to_path(path["file"])
@@ -276,7 +232,7 @@ def prepare_pip_source_args(sources, pip_args=None):
pip_args = []
if sources:
# Add the source to pip9.
pip_args.extend(["-i", sources[0]["url"]]) # type: ignore
pip_args.extend(["-i ", sources[0]["url"]]) # type: ignore
# Trust the host if it's not verified.
if not sources[0].get("verify_ssl", True):
pip_args.extend(
@@ -375,14 +331,10 @@ class PathAccessError(KeyError, IndexError, TypeError):
def __repr__(self):
cn = self.__class__.__name__
return "%s(%r, %r, %r)" % (cn, self.exc, self.seg, self.path)
return f"{cn}({self.exc!r}, {self.seg!r}, {self.path!r})"
def __str__(self):
return "could not access %r from path %r, got error: %r" % (
self.seg,
self.path,
self.exc,
)
return f"could not access {self.seg} from path {self.path}, got error: {self.exc}"
def get_path(root, path, default=_UNSET):
@@ -421,7 +373,7 @@ def get_path(root, path, default=_UNSET):
cur = cur[seg]
except (KeyError, IndexError) as exc:
raise PathAccessError(exc, seg, path)
except TypeError as exc:
except TypeError:
# either string index in a list, or a parent that
# doesn't support indexing
try:
@@ -701,20 +653,90 @@ def get_pip_command() -> InstallCommand:
# General options (find_links, index_url, extra_index_url, trusted_host,
# and pre) are deferred to pip.
pip_command = InstallCommand(
name="InstallCommand", summary="requirementslib pip Install command."
name="InstallCommand", summary="pipenv pip Install command."
)
return pip_command
# Borrowed from Pew.
# See https://github.com/berdario/pew/blob/master/pew/_utils.py#L82
@contextmanager
def temp_environ():
# type: () -> Iterator[None]
"""Allow the ability to set os.environ temporarily."""
environ = dict(os.environ)
try:
yield
finally:
os.environ.clear()
os.environ.update(environ)
def unpack_url(
link: Link,
location: str,
download: Downloader,
verbosity: int,
download_dir: Optional[str] = None,
hashes: Optional[Hashes] = None,
) -> Optional[File]:
"""Unpack link into location, downloading if required.
:param hashes: A Hashes object, one of whose embedded hashes must match,
or HashMismatch will be raised. If the Hashes is empty, no matches are
required, and unhashable types of requirements (like VCS ones, which
would ordinarily raise HashUnsupported) are allowed.
"""
# non-editable vcs urls
if link.scheme in [
"git+http",
"git+https",
"git+ssh",
"git+git",
"hg+http",
"hg+https",
"hg+ssh",
"svn+http",
"svn+https",
"svn+svn",
"bzr+http",
"bzr+https",
"bzr+ssh",
"bzr+sftp",
"bzr+ftp",
"bzr+lp",
]:
unpack_vcs_link(link, location, verbosity=verbosity)
return File(location, content_type=None)
assert not link.is_existing_dir()
# file urls
if link.is_file:
file = get_file_url(link, download_dir, hashes=hashes)
# http urls
else:
file = get_http_url(
link,
download,
download_dir,
hashes=hashes,
)
# unpack the archive to the build dir location. even when only downloading
# archives, they have to be unpacked to parse dependencies, except wheels
if not link.is_wheel:
unpack_file(file.path, location, file.content_type)
return file
def get_http_url(
link: Link,
download: Downloader,
download_dir: Optional[str] = None,
hashes: Optional[Hashes] = None,
) -> File:
temp_dir = TempDirectory(kind="unpack", globally_managed=False)
# If a download dir is specified, is the file already downloaded there?
already_downloaded_path = None
if download_dir:
already_downloaded_path = _check_download_dir(link, download_dir, hashes)
if already_downloaded_path:
from_path = already_downloaded_path
content_type = None
else:
# let's download to a tmp dir
from_path, content_type = download(link, temp_dir.path)
if hashes:
hashes.check_against_path(from_path)
return File(from_path, content_type)
+180 -508
View File
@@ -1,5 +1,4 @@
import contextlib
import hashlib
import json
import os
import subprocess
@@ -7,18 +6,15 @@ import sys
import tempfile
import warnings
from functools import lru_cache
from html.parser import HTMLParser
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Union
from urllib import parse
from typing import Dict, List, Optional, Set
from pipenv import environments, resolver
from pipenv.exceptions import RequirementError, ResolutionFailure
from pipenv.exceptions import ResolutionFailure
from pipenv.patched.pip._internal.cache import WheelCache
from pipenv.patched.pip._internal.commands.install import InstallCommand
from pipenv.patched.pip._internal.exceptions import InstallationError
from pipenv.patched.pip._internal.models.target_python import TargetPython
from pipenv.patched.pip._internal.network.cache import SafeFileCache
from pipenv.patched.pip._internal.operations.build.build_tracker import (
get_build_tracker,
)
@@ -26,14 +22,13 @@ from pipenv.patched.pip._internal.req.constructors import (
install_req_from_parsed_requirement,
)
from pipenv.patched.pip._internal.req.req_file import parse_requirements
from pipenv.patched.pip._internal.utils.hashes import FAVORITE_HASH
from pipenv.patched.pip._internal.req.req_install import InstallRequirement
from pipenv.patched.pip._internal.utils.temp_dir import global_tempdir_manager
from pipenv.patched.pip._vendor import pkg_resources, rich
from pipenv.project import Project
from pipenv.utils.fileutils import create_tracked_tempdir
from pipenv.utils.requirements import normalize_name
from pipenv.vendor import click
from pipenv.vendor.requirementslib.fileutils import create_tracked_tempdir, open_file
from pipenv.vendor.requirementslib.models.requirements import Line, Requirement
from pipenv.vendor.requirementslib.models.utils import DIRECT_URL_RE
try:
# this is only in Python3.8 and later
@@ -44,18 +39,17 @@ except ImportError:
from .dependencies import (
HackedPythonVersion,
clean_pkg_version,
convert_deps_to_pip,
determine_package_name,
expansive_install_req_from_line,
get_constraints_from_deps,
get_lockfile_section_using_pipfile_category,
get_vcs_deps,
is_pinned_requirement,
pep423_name,
prepare_constraint_file,
translate_markers,
)
from .indexes import parse_indexes, prepare_pip_source_args
from .internet import _get_requests_session, is_pypi_url
from .internet import is_pypi_url
from .locking import format_requirement_for_lockfile, prepare_lockfile
from .shell import make_posix, subprocess_run, temp_environ
@@ -101,42 +95,14 @@ class HashCacheMixin:
avoid issues where the location on the server changes.
"""
def __init__(self, directory, session):
def __init__(self, project, session):
self.project = project
self.session = session
if not os.path.isdir(directory):
os.makedirs(directory, exist_ok=True)
super().__init__(directory=directory)
def get_hash(self, link):
# If there is no link hash (i.e., md5, sha256, etc.), we don't want
# to store it.
hash_value = self.get(link.url)
if not hash_value:
hash_value = self._get_file_hash(link).encode()
self.set(link.url, hash_value)
hash_value = self.project.get_file_hash(self.session, link).encode()
return hash_value.decode("utf8")
def _get_file_hash(self, link):
h = hashlib.new(FAVORITE_HASH)
with open_file(link.url, self.session) as fp:
for chunk in iter(lambda: fp.read(8096), b""):
h.update(chunk)
return f"{h.name}:{h.hexdigest()}"
class PackageIndexHTMLParser(HTMLParser):
def __init__(self):
super().__init__()
self.urls = []
def handle_starttag(self, tag, attrs):
# If tag is an anchor
if tag == "a":
# find href attribute
for attr in attrs:
if attr[0] == "href":
self.urls.append(attr[1])
class Resolver:
def __init__(
@@ -151,6 +117,9 @@ class Resolver:
clear=False,
pre=False,
category=None,
original_deps=None,
install_reqs=None,
pipfile_entries=None,
):
self.initial_constraints = constraints
self.req_dir = req_dir
@@ -167,18 +136,11 @@ class Resolver:
self.skipped = skipped if skipped is not None else {}
self.markers = {}
self.requires_python_markers = {}
self._pip_args = None
self._constraints = None
self._parsed_constraints = None
self._resolver = None
self._finder = None
self._session = None
self._constraint_file = None
self._pip_options = None
self._pip_command = None
self.original_deps = original_deps if original_deps is not None else {}
self.install_reqs = install_reqs if install_reqs is not None else {}
self.pipfile_entries = pipfile_entries if pipfile_entries is not None else {}
self._retry_attempts = 0
self._hash_cache = None
self._sessions = {}
def __repr__(self):
return (
@@ -194,260 +156,25 @@ class Resolver:
@property
def hash_cache(self):
if not self._hash_cache:
self._hash_cache = type("HashCache", (HashCacheMixin, SafeFileCache), {})(
os.path.join(self.project.s.PIPENV_CACHE_DIR, "hashes"), self.session
)
self._hash_cache = HashCacheMixin(self.project, self.session)
return self._hash_cache
def get_metadata(
def check_if_package_req_skipped(
self,
deps: List[str],
index_lookup: Dict[str, str],
markers_lookup: Dict[str, str],
project: Project,
sources: Dict[str, str],
req_dir: Optional[str] = None,
pre: bool = False,
clear: bool = False,
category: str = None,
) -> Tuple[
Set[str],
Dict[str, Dict[str, Union[str, bool, List[str]]]],
Dict[str, str],
Dict[str, str],
]:
constraints: Set[str] = set()
skipped: Dict[str, Dict[str, Union[str, bool, List[str]]]] = {}
if index_lookup is None:
index_lookup = {}
if markers_lookup is None:
markers_lookup = {}
if not req_dir:
req_dir = create_tracked_tempdir(prefix="pipenv-", suffix="-reqdir")
for dep in deps:
if not dep:
continue
req, req_idx, markers_idx = self.parse_line(
dep,
index_lookup=index_lookup,
markers_lookup=markers_lookup,
project=project,
req: InstallRequirement,
) -> bool:
if req.markers and not req.markers.evaluate():
err.print(
f"Could not find a matching version of {req}; {req.markers} for your environment, "
"its dependencies will be skipped.",
)
index_lookup.update(req_idx)
markers_lookup.update(markers_idx)
# Add dependencies of any file (e.g. wheels/tarballs), source, or local
# directories into the initial constraint pool to be resolved with the
# rest of the dependencies, while adding the files/vcs deps/paths themselves
# to the lockfile directly
use_sources = None
if req.name in index_lookup:
use_sources = list(
filter(lambda s: s.get("name") == index_lookup[req.name], sources)
)
if not use_sources:
use_sources = sources
transient_resolver = Resolver(
[],
req_dir,
project,
use_sources,
index_lookup=index_lookup,
markers_lookup=markers_lookup,
clear=clear,
pre=pre,
category=category,
)
constraint_update, lockfile_update = self.get_deps_from_req(
req, resolver=transient_resolver, resolve_vcs=project.s.PIPENV_RESOLVE_VCS
)
constraints |= constraint_update
skipped.update(lockfile_update)
return constraints, skipped, index_lookup, markers_lookup
def parse_line(
self,
line: str,
index_lookup: Dict[str, str] = None,
markers_lookup: Dict[str, str] = None,
project: Optional[Project] = None,
) -> Tuple[Requirement, Dict[str, str], Dict[str, str]]:
if index_lookup is None:
index_lookup = {}
if markers_lookup is None:
markers_lookup = {}
if project is None:
from pipenv.project import Project
project = Project()
index, extra_index, trust_host, remainder = parse_indexes(line)
line = " ".join(remainder)
try:
req = Requirement.from_line(line)
except ValueError:
direct_url = DIRECT_URL_RE.match(line)
if direct_url:
name = direct_url.groupdict()["name"]
line = f"{name}@ {line}"
try:
req = Requirement.from_line(line)
except ValueError:
raise ResolutionFailure(
f"Failed to resolve requirement from line: {line!s}"
)
else:
raise ResolutionFailure(
f"Failed to resolve requirement from line: {line!s}"
)
if index:
try:
index_lookup[req.normalized_name] = project.get_source(
url=index, refresh=True
).get("name")
except TypeError:
pass
try:
req.normalized_name
except TypeError:
raise RequirementError(req=req)
# strip the marker and re-add it later after resolution
# but we will need a fallback in case resolution fails
# eg pypiwin32
if req.markers:
markers_lookup[req.normalized_name] = req.markers.replace('"', "'")
return req, index_lookup, markers_lookup
def get_deps_from_req(
self,
req: Requirement,
resolver: Optional["Resolver"] = None,
resolve_vcs: bool = True,
) -> Tuple[Set[str], Dict[str, Dict[str, Union[str, bool, List[str]]]]]:
from pipenv.vendor.requirementslib.models.requirements import Requirement
from pipenv.vendor.requirementslib.models.utils import (
_requirement_to_str_lowercase_name,
)
from pipenv.vendor.requirementslib.utils import is_installable_dir
# TODO: this is way too complex, refactor this
constraints: Set[str] = set()
locked_deps: Dict[str, Dict[str, Union[str, bool, List[str]]]] = {}
editable_packages = self.project.get_editable_packages(category=self.category)
if (req.is_file_or_url or req.is_vcs) and not req.is_wheel:
# for local packages with setup.py files and potential direct url deps:
if req.is_vcs:
req_list, lockfile = get_vcs_deps(reqs=[req])
req = next(iter(req for req in req_list if req is not None), req_list)
entry = lockfile[pep423_name(req.normalized_name)]
else:
_, entry = req.pipfile_entry
parsed_line: Line = req.req.parsed_line
try:
name = req.normalized_name
except TypeError:
raise RequirementError(req=req)
if parsed_line.setup_info:
setup_info = parsed_line.setup_info
else:
setup_info = req.req.parse_setup_info()
locked_deps[pep423_name(name)] = entry
requirements = []
# Allow users to toggle resolution off for non-editable VCS packages
# but leave it on for local, installable folders on the filesystem
if resolve_vcs or (
req.editable
or parsed_line.is_wheel
or (
req.is_file_or_url
and parsed_line.is_local
and is_installable_dir(parsed_line.path)
)
):
setup_info.run_pyproject()
setup_info.run_setup()
requirements = [v for v in getattr(setup_info, "requires", {}).values()]
if req.extras:
for extra in req.extras:
requirements.extend(
v
for v in getattr(setup_info, "extras", {}).get(extra, [])
if v not in requirements
)
for r in requirements:
if getattr(r, "url", None) and not getattr(r, "editable", False):
if r is not None:
if not r.url:
continue
line = _requirement_to_str_lowercase_name(r)
new_req, _, _ = self.parse_line(line)
if r.marker and not r.marker.evaluate():
new_constraints = {}
_, new_entry = req.pipfile_entry
new_lock = {pep423_name(new_req.normalized_name): new_entry}
else:
new_constraints, new_lock = self.get_deps_from_req(
new_req, resolver
)
locked_deps.update(new_lock)
constraints |= new_constraints
# if there is no marker or there is a valid marker, add the constraint line
elif r and (not r.marker or (r.marker and r.marker.evaluate())):
if r.name not in editable_packages:
line = _requirement_to_str_lowercase_name(r)
constraints.add(line)
# ensure the top level entry remains as provided
# note that we shouldn't pin versions for editable vcs deps
if not req.is_vcs:
if req.specifiers:
locked_deps[name]["version"] = req.specifiers
elif parsed_line.setup_info and parsed_line.setup_info.version:
locked_deps[name]["version"] = f"=={parsed_line.setup_info.version}"
# if not req.is_vcs:
locked_deps.update({name: entry})
else:
# if the dependency isn't installable, don't add it to constraints
# and instead add it directly to the lock
if (
req
and req.requirement
and (req.requirement.marker and not req.requirement.marker.evaluate())
):
pypi = resolver.finder if resolver else None
ireq = req.ireq
best_match = (
pypi.find_best_candidate(ireq.name, ireq.specifier).best_candidate
if pypi
else None
)
if best_match:
ireq.req.specifier = ireq.specifier.__class__(
f"=={best_match.version}"
)
hashes = resolver.collect_hashes(ireq) if resolver else []
new_req = Requirement.from_ireq(ireq)
new_req.add_hashes(hashes)
new_req.merge_markers(req.markers)
name, entry = new_req.pipfile_entry
locked_deps[pep423_name(name)] = translate_markers(entry)
click.echo(
"{} doesn't match your environment, "
"its dependencies won't be resolved.".format(req.as_line()),
err=True,
)
else:
click.echo(
"Could not find a version of {} that matches your environment, "
"it will be skipped.".format(req.as_line()),
err=True,
)
return constraints, locked_deps
constraints.add(req.constraint_line)
return constraints, locked_deps
return constraints, locked_deps
return True
return False
@classmethod
def create(
cls,
deps: List[str],
deps: Set[str],
project: Project,
index_lookup: Dict[str, str] = None,
markers_lookup: Dict[str, str] = None,
@@ -463,41 +190,68 @@ class Resolver:
index_lookup = {}
if markers_lookup is None:
markers_lookup = {}
original_deps = {}
install_reqs = {}
pipfile_entries = {}
skipped = {}
if sources is None:
sources = project.sources
packages = project.get_pipfile_section(category)
constraints = set()
for dep in deps: # Build up the index and markers lookups
if not dep:
continue
is_constraint = True
install_req = expansive_install_req_from_line(dep, expand_env=True)
package_name = determine_package_name(install_req)
original_deps[package_name] = dep
install_reqs[package_name] = install_req
index, extra_index, trust_host, remainder = parse_indexes(dep)
if package_name in packages:
pipfile_entry = packages[package_name]
pipfile_entries[package_name] = pipfile_entry
if isinstance(pipfile_entry, dict):
if packages[package_name].get("index"):
index_lookup[package_name] = packages[package_name].get("index")
if packages[package_name].get("skip_resolver"):
is_constraint = False
skipped[package_name] = dep
elif index:
index_lookup[package_name] = index
else:
index_lookup[package_name] = project.get_default_index()["name"]
if install_req.markers:
markers_lookup[package_name] = install_req.markers
if is_constraint:
constraints.add(dep)
resolver = Resolver(
[],
set(),
req_dir,
project,
sources,
index_lookup=index_lookup,
markers_lookup=markers_lookup,
skipped=skipped,
clear=clear,
pre=pre,
category=category,
original_deps=original_deps,
install_reqs=install_reqs,
pipfile_entries=pipfile_entries,
)
constraints, skipped, index_lookup, markers_lookup = resolver.get_metadata(
deps,
index_lookup,
markers_lookup,
project,
sources,
req_dir=req_dir,
pre=pre,
clear=clear,
category=category,
) # Workaround to the fact `get_metadata` instantiates a transient Resolver
for package_name, dep in original_deps.items():
install_req = install_reqs[package_name]
if resolver.check_if_package_req_skipped(install_req):
resolver.skipped[package_name] = dep
resolver.initial_constraints = constraints
resolver.skipped = skipped
resolver.index_lookup = index_lookup
resolver.finder.index_lookup = index_lookup
resolver.markers_lookup = markers_lookup
return resolver
@property
def pip_command(self):
if self._pip_command is None:
self._pip_command = self._get_pip_command()
return self._pip_command
return self._get_pip_command()
def prepare_pip_args(self, use_pep517=None, build_isolation=True):
pip_args = []
@@ -516,11 +270,9 @@ class Resolver:
def pip_args(self):
use_pep517 = environments.get_from_env("USE_PEP517", prefix="PIP")
build_isolation = environments.get_from_env("BUILD_ISOLATION", prefix="PIP")
if self._pip_args is None:
self._pip_args = self.prepare_pip_args(
use_pep517=use_pep517, build_isolation=build_isolation
)
return self._pip_args
return self.prepare_pip_args(
use_pep517=use_pep517, build_isolation=build_isolation
)
def prepare_constraint_file(self):
constraint_filename = prepare_constraint_file(
@@ -533,9 +285,7 @@ class Resolver:
@property
def constraint_file(self):
if self._constraint_file is None:
self._constraint_file = self.prepare_constraint_file()
return self._constraint_file
return self.prepare_constraint_file()
@cached_property
def default_constraint_file(self):
@@ -550,24 +300,20 @@ class Resolver:
@property
def pip_options(self):
if self._pip_options is None:
pip_options, _ = self.pip_command.parser.parse_args(self.pip_args)
pip_options.cache_dir = self.project.s.PIPENV_CACHE_DIR
pip_options.no_python_version_warning = True
pip_options.no_input = self.project.settings.get("disable_pip_input", True)
pip_options.progress_bar = "off"
pip_options.ignore_requires_python = True
pip_options.pre = self.pre or self.project.settings.get(
"allow_prereleases", False
)
self._pip_options = pip_options
return self._pip_options
pip_options, _ = self.pip_command.parser.parse_args(self.pip_args)
pip_options.cache_dir = self.project.s.PIPENV_CACHE_DIR
pip_options.no_python_version_warning = True
pip_options.no_input = self.project.settings.get("disable_pip_input", True)
pip_options.progress_bar = "off"
pip_options.ignore_requires_python = True
pip_options.pre = self.pre or self.project.settings.get(
"allow_prereleases", False
)
return pip_options
@property
def session(self):
if self._session is None:
self._session = self.pip_command._build_session(self.pip_options)
return self._session
return self.pip_command._build_session(self.pip_options)
def prepare_index_lookup(self):
index_mapping = {}
@@ -582,29 +328,26 @@ class Resolver:
@property
def finder(self):
if self._finder is None:
self._finder = get_package_finder(
install_cmd=self.pip_command,
options=self.pip_options,
session=self.session,
)
finder = get_package_finder(
install_cmd=self.pip_command,
options=self.pip_options,
session=self.session,
)
index_lookup = self.prepare_index_lookup()
self._finder._link_collector.index_lookup = index_lookup
self._finder._link_collector.search_scope.index_lookup = index_lookup
return self._finder
finder._link_collector.index_lookup = index_lookup
finder._link_collector.search_scope.index_lookup = index_lookup
return finder
@property
def parsed_constraints(self):
pip_options = self.pip_options
pip_options.extra_index_urls = []
if self._parsed_constraints is None:
self._parsed_constraints = parse_requirements(
self.constraint_file,
finder=self.finder,
session=self.session,
options=pip_options,
)
return self._parsed_constraints
return parse_requirements(
self.constraint_file,
finder=self.finder,
session=self.session,
options=pip_options,
)
@cached_property
def parsed_default_constraints(self):
@@ -617,11 +360,11 @@ class Resolver:
session=self.session,
options=pip_options,
)
return parsed_default_constraints
return set(parsed_default_constraints)
@cached_property
def default_constraints(self):
default_constraints = [
possible_default_constraints = [
install_req_from_parsed_requirement(
c,
isolated=self.pip_options.build_isolation,
@@ -629,25 +372,34 @@ class Resolver:
)
for c in self.parsed_default_constraints
]
return default_constraints
default_constraints = []
for c in possible_default_constraints:
default_constraints.append(c)
return set(default_constraints)
@property
def possible_constraints(self):
possible_constraints_list = [
install_req_from_parsed_requirement(
c,
isolated=self.pip_options.build_isolation,
use_pep517=self.pip_options.use_pep517,
user_supplied=True,
)
for c in self.parsed_constraints
]
return possible_constraints_list
@property
def constraints(self):
if self._constraints is None:
self._constraints = [
install_req_from_parsed_requirement(
c,
isolated=self.pip_options.build_isolation,
use_pep517=self.pip_options.use_pep517,
user_supplied=True,
)
for c in self.parsed_constraints
]
# Only use default_constraints when installing dev-packages
if self.category != "packages":
self._constraints += self.default_constraints
self._constraints.sort(key=lambda ireq: ireq.name)
return self._constraints
possible_constraints_list = self.possible_constraints
constraints_list = set()
for c in possible_constraints_list:
constraints_list.add(c)
# Only use default_constraints when installing dev-packages
if self.category != "packages":
constraints_list |= self.default_constraints
return set(constraints_list)
@contextlib.contextmanager
def get_resolver(self, clear=False):
@@ -682,7 +434,6 @@ class Resolver:
yield resolver
def resolve(self):
self.constraints # For some reason it is important to evaluate constraints before resolver context
with temp_environ(), self.get_resolver() as resolver:
try:
results = resolver.resolve(self.constraints, check_supported_wheels=False)
@@ -694,7 +445,7 @@ class Resolver:
return self.resolved_tree
def resolve_constraints(self):
from pipenv.vendor.requirementslib.models.markers import marker_from_specifier
from .markers import marker_from_specifier
new_tree = set()
for result in self.resolved_tree:
@@ -723,100 +474,10 @@ class Resolver:
new_tree.add(result)
self.resolved_tree = new_tree
@classmethod
def prepend_hash_types(cls, checksums, hash_type):
cleaned_checksums = set()
for checksum in checksums:
if not checksum:
continue
if not checksum.startswith(f"{hash_type}:"):
checksum = f"{hash_type}:{checksum}"
cleaned_checksums.add(checksum)
return cleaned_checksums
def _get_requests_session_for_source(self, source):
if self._sessions.get(source["name"]):
session = self._sessions[source["name"]]
else:
session = _get_requests_session(
self.project.s.PIPENV_MAX_RETRIES, source.get("verify_ssl", True)
)
self._sessions[source["name"]] = session
return session
def _get_hashes_from_pypi(self, ireq, source):
pkg_url = f"https://pypi.org/pypi/{ireq.name}/json"
session = self._get_requests_session_for_source(source)
try:
collected_hashes = set()
# Grab the hashes from the new warehouse API.
r = session.get(pkg_url, timeout=10)
api_releases = r.json()["releases"]
cleaned_releases = {}
for api_version, api_info in api_releases.items():
api_version = clean_pkg_version(api_version)
cleaned_releases[api_version] = api_info
version = ""
if ireq.specifier:
spec = next(iter(s for s in ireq.specifier), None)
if spec:
version = spec.version
for release in cleaned_releases[version]:
collected_hashes.add(release["digests"][FAVORITE_HASH])
return self.prepend_hash_types(collected_hashes, FAVORITE_HASH)
except (ValueError, KeyError, ConnectionError):
if self.project.s.is_verbose():
click.echo(
"{}: Error generating hash for {}".format(
click.style("Warning", bold=True, fg="red"), ireq.name
),
err=True,
)
return None
def _get_hashes_from_remote_index_urls(self, ireq, source):
pkg_url = f"{source['url']}/{ireq.name}/"
session = self._get_requests_session_for_source(source)
try:
collected_hashes = set()
# Grab the hashes from the new warehouse API.
response = session.get(pkg_url, timeout=10)
# Create an instance of the parser
parser = PackageIndexHTMLParser()
# Feed the HTML to the parser
parser.feed(response.text)
# Extract hrefs
hrefs = parser.urls
version = ""
if ireq.specifier:
spec = next(iter(s for s in ireq.specifier), None)
if spec:
version = spec.version
for package_url in hrefs:
if version in package_url:
url_params = parse.urlparse(package_url).fragment
params_dict = parse.parse_qs(url_params)
if params_dict.get(FAVORITE_HASH):
collected_hashes.add(params_dict[FAVORITE_HASH][0])
return self.prepend_hash_types(collected_hashes, FAVORITE_HASH)
except (ValueError, KeyError, ConnectionError):
if self.project.s.is_verbose():
click.echo(
"{}: Error generating hash for {}".format(
click.style("Warning", bold=True, fg="red"), ireq.name
),
err=True,
)
return None
def collect_hashes(self, ireq):
link = ireq.link # Handle VCS and file links first
if link and (link.is_vcs or (link.is_file and link.is_existing_dir())):
return set()
if link and ireq.original_link:
return {self._get_hash_from_link(ireq.original_link)}
if not is_pinned_requirement(ireq):
return set()
@@ -828,11 +489,11 @@ class Resolver:
source = sources[0] if len(sources) else None
if source:
if is_pypi_url(source["url"]):
hashes = self._get_hashes_from_pypi(ireq, source)
hashes = self.project.get_hashes_from_pypi(ireq, source)
if hashes:
return hashes
else:
hashes = self._get_hashes_from_remote_index_urls(ireq, source)
hashes = self.project.get_hashes_from_remote_index_urls(ireq, source)
if hashes:
return hashes
@@ -841,12 +502,14 @@ class Resolver:
).iter_applicable()
applicable_candidates = list(applicable_candidates)
if applicable_candidates:
return {
self._get_hash_from_link(candidate.link)
for candidate in applicable_candidates
}
return sorted(
{
self.project.get_hash_from_link(self.hash_cache, candidate.link)
for candidate in applicable_candidates
}
)
if link:
return {self._get_hash_from_link(link)}
return {self.project.get_hash_from_link(self.hash_cache, link)}
return set()
def resolve_hashes(self):
@@ -855,19 +518,18 @@ class Resolver:
self.hashes[ireq] = self.collect_hashes(ireq)
return self.hashes
def _get_hash_from_link(self, link):
if link.hash and link.hash_name == FAVORITE_HASH:
return f"{link.hash_name}:{link.hash}"
return self.hash_cache.get_hash(link)
def _clean_skipped_result(self, req, value):
def clean_skipped_result(
self, req_name: str, ireq: InstallRequirement, pipfile_entry
):
ref = None
if req.is_vcs:
ref = req.commit_hash
ireq = req.ireq
entry = value.copy()
entry["name"] = req.name
if ireq.link and ireq.link.is_vcs:
ref = ireq.link.egg_fragment
if isinstance(pipfile_entry, dict):
entry = pipfile_entry.copy()
else:
entry = {}
entry["name"] = req_name
if entry.get("editable", False) and entry.get("version"):
del entry["version"]
ref = ref if ref is not None else entry.get("ref")
@@ -876,31 +538,35 @@ class Resolver:
collected_hashes = self.collect_hashes(ireq)
if collected_hashes:
entry["hashes"] = sorted(set(collected_hashes))
return req.name, entry
return req_name, entry
def clean_results(self):
reqs = [(Requirement.from_ireq(ireq), ireq) for ireq in self.resolved_tree]
reqs = [(ireq,) for ireq in self.resolved_tree]
results = {}
for req, ireq in reqs:
if req.vcs and req.editable and not req.is_direct_url:
continue
elif req.normalized_name in self.skipped.keys():
for (ireq,) in reqs:
if normalize_name(ireq.name) in self.skipped.keys():
continue
collected_hashes = self.hashes.get(ireq, set())
req.add_hashes(collected_hashes)
if collected_hashes:
collected_hashes = sorted(collected_hashes)
name, entry = format_requirement_for_lockfile(
req, self.markers_lookup, self.index_lookup, collected_hashes
ireq,
self.markers_lookup,
self.index_lookup,
self.original_deps,
self.pipfile_entries,
collected_hashes,
)
entry = translate_markers(entry)
if name in results:
results[name].update(entry)
else:
results[name] = entry
for k in list(self.skipped.keys()):
req = Requirement.from_pipfile(k, self.skipped[k])
name, entry = self._clean_skipped_result(req, self.skipped[k])
for req_name in self.skipped:
install_req = self.install_reqs[req_name]
name, entry = self.clean_skipped_result(
req_name, install_req, self.pipfile_entries[req_name]
)
entry = translate_markers(entry)
if name in results:
results[name].update(entry)
@@ -957,7 +623,7 @@ def actually_resolve_deps(
warning.lineno,
warning.line,
)
return (results, hashes, resolver.markers_lookup, resolver, resolver.skipped)
return (results, hashes, resolver)
def resolve(cmd, st, project):
@@ -999,6 +665,7 @@ def venv_resolve_deps(
pypi_mirror=None,
pipfile=None,
lockfile=None,
old_lock_data=None,
):
"""
Resolve dependencies for a pipenv project, acts as a portal to the target environment.
@@ -1008,7 +675,7 @@ def venv_resolve_deps(
dependency resolution. This function reads the output of that call and mutates
the provided lockfile accordingly, returning nothing.
:param List[:class:`~requirementslib.Requirement`] deps: A list of dependencies to resolve.
:param List[:class:`~pip.InstallRequirement`] deps: A list of dependencies to resolve.
:param Callable which: [description]
:param project: The pipenv Project instance to use during resolution
:param Optional[bool] pre: Whether to resolve pre-release candidates, defaults to False
@@ -1036,6 +703,8 @@ def venv_resolve_deps(
pipfile = getattr(project, category, {})
if lockfile is None:
lockfile = project.lockfile(categories=[category])
if old_lock_data is None:
old_lock_data = lockfile.get(lockfile_section, {})
req_dir = create_tracked_tempdir(prefix="pipenv", suffix="requirements")
results = []
with temp_environ():
@@ -1054,9 +723,10 @@ def venv_resolve_deps(
# dependency resolution on them, so we are including this step inside the
# spinner context manager for the UX improvement
st.console.print("Building requirements...")
deps = convert_deps_to_pip(deps, project, include_index=True)
deps = convert_deps_to_pip(
deps, project.pipfile_sources(), include_index=True
)
constraints = set(deps)
st.console.print("Resolving dependencies...")
# Useful for debugging and hitting breakpoints in the resolver
if project.s.PIPENV_RESOLVER_PARENT_PYTHON:
try:
@@ -1079,7 +749,7 @@ def venv_resolve_deps(
st.console.print(
environments.PIPENV_SPINNER_FAIL_TEXT.format("Locking Failed!")
)
raise
raise # maybe sys.exit(1) here?
else: # Default/Production behavior is to use project python's resolver
cmd = [
which("python", allow_global=allow_global),
@@ -1094,6 +764,8 @@ def venv_resolve_deps(
if category:
cmd.append("--category")
cmd.append(category)
if project.s.is_verbose():
cmd.append("--verbose")
target_file = tempfile.NamedTemporaryFile(
prefix="resolver", suffix=".json", delete=False
)
@@ -1133,7 +805,9 @@ def venv_resolve_deps(
click.echo(f"Error: {c.stderr.strip()}", err=True)
if lockfile_section not in lockfile:
lockfile[lockfile_section] = {}
return prepare_lockfile(results, pipfile, lockfile[lockfile_section])
return prepare_lockfile(
project, results, pipfile, lockfile[lockfile_section], old_lock_data
)
def resolve_deps(
@@ -1164,7 +838,7 @@ def resolve_deps(
req_dir = create_tracked_tempdir(prefix="pipenv-", suffix="-requirements")
with HackedPythonVersion(python_path=project.python(system=allow_global)):
try:
results, hashes, markers_lookup, resolver, skipped = actually_resolve_deps(
results, hashes, internal_resolver = actually_resolve_deps(
deps,
index_lookup,
markers_lookup,
@@ -1189,9 +863,7 @@ def resolve_deps(
(
results,
hashes,
markers_lookup,
resolver,
skipped,
internal_resolver,
) = actually_resolve_deps(
deps,
index_lookup,
@@ -1205,7 +877,7 @@ def resolve_deps(
)
except RuntimeError:
sys.exit(1)
return results, resolver
return results, internal_resolver
@lru_cache()
+1 -1
View File
@@ -11,9 +11,9 @@ from contextlib import contextmanager
from functools import lru_cache
from pathlib import Path
from pipenv.utils.fileutils import normalize_drive, normalize_path
from pipenv.vendor import click
from pipenv.vendor.pythonfinder.utils import ensure_path
from pipenv.vendor.requirementslib.fileutils import normalize_drive, normalize_path
from .constants import FALSE_VALUES, SCHEME_LIST, TRUE_VALUES
from .processes import subprocess_run
+64
View File
@@ -1,3 +1,9 @@
from typing import Union
from pipenv.vendor.plette.models import Package, PackageCollection
from pipenv.vendor.tomlkit.container import Container
from pipenv.vendor.tomlkit.items import AoT, Array, Bool, InlineTable, Item, String, Table
try:
import tomllib as toml
except ImportError:
@@ -5,6 +11,10 @@ except ImportError:
from pipenv.vendor import tomlkit
TOML_DICT_TYPES = Union[Container, Package, PackageCollection, Table, InlineTable]
TOML_DICT_OBJECTS = (Container, Package, Table, InlineTable, PackageCollection)
TOML_DICT_NAMES = [o.__class__.__name__ for o in TOML_DICT_OBJECTS]
def cleanup_toml(tml):
toml = tml.split("\n")
@@ -74,3 +84,57 @@ def convert_toml_outline_tables(parsed, project):
parsed[section] = result
return parsed
def tomlkit_value_to_python(toml_value):
# type: (Union[Array, AoT, TOML_DICT_TYPES, Item]) -> Union[List, Dict]
value_type = type(toml_value).__name__
if (
isinstance(toml_value, TOML_DICT_OBJECTS + (dict,))
or value_type in TOML_DICT_NAMES
):
return tomlkit_dict_to_python(toml_value)
elif isinstance(toml_value, AoT) or value_type == "AoT":
return [tomlkit_value_to_python(val) for val in toml_value._body]
elif isinstance(toml_value, Array) or value_type == "Array":
return [tomlkit_value_to_python(val) for val in list(toml_value)]
elif isinstance(toml_value, String) or value_type == "String":
return f"{toml_value!s}"
elif isinstance(toml_value, Bool) or value_type == "Bool":
return toml_value.value
elif isinstance(toml_value, Item):
return toml_value.value
return toml_value
def tomlkit_dict_to_python(toml_dict):
# type: (TOML_DICT_TYPES) -> Dict
value_type = type(toml_dict).__name__
if toml_dict is None:
raise TypeError("Invalid type NoneType when converting toml dict to python")
converted = None # type: Optional[Dict]
if isinstance(toml_dict, (InlineTable, Table)) or value_type in (
"InlineTable",
"Table",
):
converted = toml_dict.value
elif isinstance(toml_dict, (Package, PackageCollection)) or value_type in (
"Package, PackageCollection"
):
converted = toml_dict._data
if isinstance(converted, Container) or type(converted).__name__ == "Container":
converted = converted.value
elif isinstance(toml_dict, Container) or value_type == "Container":
converted = toml_dict.value
elif isinstance(toml_dict, dict):
converted = toml_dict.copy()
else:
raise TypeError(
"Invalid type for conversion: expected Container, Dict, or Table, "
"got {!r}".format(toml_dict)
)
if isinstance(converted, dict):
return {k: tomlkit_value_to_python(v) for k, v in converted.items()}
elif isinstance(converted, (TOML_DICT_OBJECTS)) or value_type in TOML_DICT_NAMES:
return tomlkit_dict_to_python(converted)
return converted
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright 2019-2021 Dan Ryan and Frost Ming.
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.
-16
View File
@@ -1,16 +0,0 @@
import logging
import warnings
from .models.lockfile import Lockfile
from .models.pipfile import Pipfile
from .models.requirements import Requirement
__version__ = "3.0.0"
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
warnings.filterwarnings("ignore", category=ResourceWarning)
__all__ = ["Lockfile", "Pipfile", "Requirement"]
-17
View File
@@ -1,17 +0,0 @@
import os
from pipenv.patched.pip._vendor.platformdirs import user_cache_dir
def is_type_checking():
try:
from typing import TYPE_CHECKING
except ImportError:
return False
return TYPE_CHECKING
REQUIREMENTSLIB_CACHE_DIR = os.getenv(
"REQUIREMENTSLIB_CACHE_DIR", user_cache_dir("pipenv")
)
MYPY_RUNNING = os.environ.get("MYPY_RUNNING", is_type_checking())
-77
View File
@@ -1,77 +0,0 @@
"""A small collection of useful functional tools for working with iterables.
unnest, chunked and take are used by pipenv. However, since pipenv
relies on requirementslib, we can them here.
"""
from functools import partial
from itertools import islice, tee
from typing import Any, Iterable
def _is_iterable(elem: Any) -> bool:
if getattr(elem, "__iter__", False) or isinstance(elem, Iterable):
return True
return False
def take(n: int, iterable: Iterable) -> Iterable:
"""Take n elements from the supplied iterable without consuming it.
:param int n: Number of unique groups
:param iter iterable: An iterable to split up
"""
return list(islice(iterable, n))
def chunked(n: int, iterable: Iterable) -> Iterable:
"""Split an iterable into lists of length *n*.
:param int n: Number of unique groups
:param iter iterable: An iterable to split up
"""
return iter(partial(take, n, iter(iterable)), [])
def unnest(elem: Iterable) -> Any:
# type: (Iterable) -> Any
"""Flatten an arbitrarily nested iterable.
:param elem: An iterable to flatten
:type elem: :class:`~collections.Iterable`
>>> nested_iterable = (
1234, (3456, 4398345, (234234)), (
2396, (
928379, 29384, (
293759, 2347, (
2098, 7987, 27599
)
)
)
)
)
>>> list(unnest(nested_iterable))
[1234, 3456, 4398345, 234234, 2396, 928379, 29384, 293759,
2347, 2098, 7987, 27599]
"""
if isinstance(elem, Iterable) and not isinstance(elem, str):
elem, target = tee(elem, 2)
else:
target = elem
if not target or not _is_iterable(target):
yield target
else:
for el in target:
if isinstance(el, Iterable) and not isinstance(el, str):
el, el_copy = tee(el, 2)
for sub in unnest(el_copy):
yield sub
else:
yield el
def dedup(iterable: Iterable) -> Iterable:
# type: (Iterable) -> Iterable
"""Deduplicate an iterable object like iter(set(iterable)) but order-
preserved."""
return iter(dict.fromkeys(iterable))
View File
-28
View File
@@ -1,28 +0,0 @@
from typing import Any, Dict
from pipenv.vendor.pydantic import BaseModel, Extra
class ReqLibBaseModel(BaseModel):
def __setattr__(self, name, value): # noqa: C901 (ignore complexity)
private_attributes = {
field_name
for field_name in self.__annotations__
if field_name.startswith("_")
}
if name in private_attributes or name in self.__fields__:
return object.__setattr__(self, name, value)
if self.__config__.extra is not Extra.allow and name not in self.__fields__:
raise ValueError(f'"{self.__class__.__name__}" object has no field "{name}"')
object.__setattr__(self, name, value)
def dict(self, *args, **kwargs) -> Dict[str, Any]:
"""The requirementslib classes make use of a lot of private attributes
which do not get serialized out to the dict by default in pydantic."""
model_dict = super().dict(*args, **kwargs)
private_attrs = {k: v for k, v in self.__dict__.items() if k.startswith("_")}
model_dict.update(private_attrs)
return model_dict
-62
View File
@@ -1,62 +0,0 @@
import atexit
import os
from pipenv.patched.pip._internal.index.package_finder import PackageFinder
from pipenv.patched.pip._vendor.platformdirs import user_cache_dir
from ..utils import get_package_finder, get_pip_command, prepare_pip_source_args
CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv"))
def is_python(section):
return section.startswith("[") and ":" in section
def get_pip_options(args=None, sources=None, pip_command=None):
"""Build a pip command from a list of sources.
:param args: positional arguments passed through to the pip parser
:param sources: A list of pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:param pip_command: A pre-built pip command instance
:type pip_command: :class:`~pipenv.patched.pip._internal.cli.base_command.Command`
:return: An instance of pip_options using the supplied arguments plus sane defaults
:rtype: :class:`~pipenv.patched.pip._internal.cli.cmdoptions`
"""
if not pip_command:
pip_command = get_pip_command()
if not sources:
sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}]
os.makedirs(CACHE_DIR, mode=0o777, exist_ok=True)
pip_args = args or []
pip_args = prepare_pip_source_args(sources, pip_args)
pip_options, _ = pip_command.parser.parse_args(pip_args)
pip_options.cache_dir = CACHE_DIR
return pip_options
def get_finder(sources=None, pip_command=None, pip_options=None) -> PackageFinder:
"""Get a package finder for looking up candidates to install.
:param sources: A list of pipfile-formatted sources, defaults to None
:param sources: list[dict], optional
:param pip_command: A pip command instance, defaults to None
:type pip_command: :class:`~pipenv.patched.pip._internal.cli.base_command.Command`
:param pip_options: A pip options, defaults to None
:type pip_options: :class:`~pipenv.patched.pip._internal.cli.cmdoptions`
:return: A package finder
:rtype: :class:`~pipenv.patched.pip._internal.index.PackageFinder`
"""
if not pip_command:
pip_command = get_pip_command()
if not sources:
sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}]
if not pip_options:
pip_options = get_pip_options(sources=sources, pip_command=pip_command)
session = pip_command._build_session(pip_options)
atexit.register(session.close)
finder = get_package_finder(get_pip_command(), options=pip_options, session=session)
return session, finder
-248
View File
@@ -1,248 +0,0 @@
import copy
import itertools
import os
from json import JSONDecodeError
from pathlib import Path
from typing import Dict, Iterator, List, Optional
from pipenv.vendor.plette import lockfiles
from pipenv.vendor.pydantic import Field
from ..exceptions import LockfileCorruptException, MissingParameter, PipfileNotFound
from ..utils import is_editable, is_vcs, merge_items
from .common import ReqLibBaseModel
from .project import ProjectFile
from .requirements import Requirement
DEFAULT_NEWLINES = "\n"
def preferred_newlines(f):
if isinstance(f.newlines, str):
return f.newlines
return DEFAULT_NEWLINES
class Lockfile(ReqLibBaseModel):
path: Path = Field(
default_factory=lambda: Path(os.curdir).joinpath("Pipfile.lock").absolute()
)
_requirements: Optional[list] = Field(default_factory=list)
_dev_requirements: Optional[list] = Field(default_factory=list)
projectfile: ProjectFile = None
lockfile: lockfiles.Lockfile
newlines: str = DEFAULT_NEWLINES
class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)
@property
def section_keys(self):
return set(self.lockfile.keys()) - {"_meta"}
@property
def extended_keys(self):
return [k for k in itertools.product(self.section_keys, ["", "vcs", "editable"])]
def get(self, k):
return self.__getitem__(k)
def __contains__(self, k):
check_lockfile = k in self.extended_keys or self.lockfile.__contains__(k)
if check_lockfile:
return True
return super(Lockfile, self).__contains__(k)
def __setitem__(self, k, v):
lockfile = self.lockfile
lockfile.__setitem__(k, v)
def __getitem__(self, k, *args, **kwargs):
retval = None
lockfile = self.lockfile
try:
retval = lockfile[k]
except KeyError:
if "-" in k:
section, _, pkg_type = k.rpartition("-")
vals = getattr(lockfile.get(section, {}), "_data", {})
if pkg_type == "vcs":
retval = {k: v for k, v in vals.items() if is_vcs(v)}
elif pkg_type == "editable":
retval = {k: v for k, v in vals.items() if is_editable(v)}
if retval is None:
raise
else:
retval = getattr(retval, "_data", retval)
return retval
def __getattr__(self, k, *args, **kwargs):
lockfile = self.lockfile
try:
return super(Lockfile, self).__getattribute__(k)
except AttributeError:
retval = getattr(lockfile, k, None)
if retval is not None:
return retval
return super(Lockfile, self).__getattribute__(k, *args, **kwargs)
def get_deps(self, dev=False, only=True):
deps = {}
if dev:
deps.update(self.develop._data)
if only:
return deps
deps = merge_items([deps, self.default._data])
return deps
@classmethod
def read_projectfile(cls, path):
pf = ProjectFile.read(path, lockfiles.Lockfile, invalid_ok=True)
return pf
@classmethod
def lockfile_from_pipfile(cls, pipfile_path):
from .pipfile import Pipfile
if os.path.isfile(pipfile_path):
if not os.path.isabs(pipfile_path):
pipfile_path = os.path.abspath(pipfile_path)
pipfile = Pipfile.load(os.path.dirname(pipfile_path))
return lockfiles.Lockfile.with_meta_from(pipfile.pipfile)
raise PipfileNotFound(pipfile_path)
@classmethod
def load_projectfile(
cls, path: Optional[str] = None, create: bool = True, data: Optional[Dict] = None
) -> "ProjectFile":
if not path:
path = os.curdir
path = Path(path).absolute()
project_path = path if path.is_dir() else path.parent
lockfile_path = path if path.is_file() else project_path / "Pipfile.lock"
if not project_path.exists():
raise OSError(f"Project does not exist: {project_path.as_posix()}")
elif not lockfile_path.exists() and not create:
raise FileNotFoundError(
f"Lockfile does not exist: {lockfile_path.as_posix()}"
)
projectfile = cls.read_projectfile(lockfile_path.as_posix())
if not lockfile_path.exists():
if not data:
pipfile = project_path.joinpath("Pipfile")
lf = cls.lockfile_from_pipfile(pipfile)
else:
lf = lockfiles.Lockfile(data)
projectfile.model = lf
else:
if data:
raise ValueError("Cannot pass data when loading existing lockfile")
with open(lockfile_path.as_posix(), "r") as f:
projectfile.model = lockfiles.Lockfile.load(f)
return projectfile
@classmethod
def from_data(
cls, path: Optional[str], data: Optional[Dict], meta_from_project: bool = True
) -> "Lockfile":
if path is None:
raise MissingParameter("path")
if data is None:
raise MissingParameter("data")
if not isinstance(data, dict):
raise TypeError("Expecting a dictionary for parameter 'data'")
path = os.path.abspath(str(path))
if os.path.isdir(path):
project_path = path
elif not os.path.isdir(path) and os.path.isdir(os.path.dirname(path)):
project_path = os.path.dirname(path)
pipfile_path = os.path.join(project_path, "Pipfile")
lockfile_path = os.path.join(project_path, "Pipfile.lock")
if meta_from_project:
lockfile = cls.lockfile_from_pipfile(pipfile_path)
lockfile.update(data)
else:
lockfile = lockfiles.Lockfile(data)
projectfile = ProjectFile(
line_ending=DEFAULT_NEWLINES, location=lockfile_path, model=lockfile
)
return cls(
projectfile=projectfile,
lockfile=lockfile,
newlines=projectfile.line_ending,
path=Path(projectfile.location),
)
@classmethod
def load(cls, path: Optional[str], create: bool = True) -> "Lockfile":
try:
projectfile = cls.load_projectfile(path, create=create)
except JSONDecodeError:
path = os.path.abspath(path)
path = Path(
os.path.join(path, "Pipfile.lock") if os.path.isdir(path) else path
)
formatted_path = path.as_posix()
backup_path = f"{formatted_path}.bak"
LockfileCorruptException.show(formatted_path, backup_path=backup_path)
path.rename(backup_path)
cls.load(formatted_path, create=True)
lockfile_path = Path(projectfile.location)
creation_args = {
"projectfile": projectfile,
"lockfile": projectfile.model,
"newlines": projectfile.line_ending,
"path": lockfile_path,
}
return cls(**creation_args)
@classmethod
def create(cls, path: Optional[str], create: bool = True) -> "Lockfile":
return cls.load(path, create=create)
def get_section(self, name: str) -> Optional[Dict]:
return self.lockfile.get(name)
@property
def develop(self) -> Dict:
return self.lockfile.develop
@property
def default(self) -> Dict:
return self.lockfile.default
def get_requirements(
self, dev: bool = True, only: bool = False, categories: Optional[List[str]] = None
) -> Iterator[Requirement]:
if categories:
deps = {}
for category in categories:
if category == "packages":
category = "default"
elif category == "dev-packages":
category = "develop"
try:
category_deps = self[category]
except KeyError:
category_deps = {}
self.lockfile[category] = category_deps
deps = merge_items([deps, category_deps])
else:
deps = self.get_deps(dev=dev, only=only)
for k, v in deps.items():
yield Requirement.from_pipfile(k, v)
def requirements_list(self, category: str) -> List[Dict]:
if self.lockfile.get(category):
return [
{name: entry._data} for name, entry in self.lockfile[category].items()
]
return []
def write(self) -> None:
self.projectfile.model = copy.deepcopy(self.lockfile)
self.projectfile.write()
File diff suppressed because it is too large Load Diff
-97
View File
@@ -1,97 +0,0 @@
"""These were old pip utils that were dropped starting in pip 22.1 but
`requirementslib` still deeply depends upon.
In the interest of getting the build working with the latest version of
pip again, this workaround to copy these dependent utils in was
provided. Ideally the code that depends on this behavior would be
modernized and refactored to not require it.
"""
import logging
import os
import shutil
import stat
logger = logging.getLogger(__name__)
from typing import Dict, Iterable, List
# This can be removed once this pr is merged
# https://github.com/python/cpython/pull/16575
def is_socket(path: str) -> bool:
return stat.S_ISSOCK(os.lstat(path).st_mode)
def copy2_fixed(src: str, dest: str) -> None:
"""Wrap shutil.copy2() but map errors copying socket files to
SpecialFileError as expected.
See also https://bugs.python.org/issue37700.
"""
try:
shutil.copy2(src, dest)
except OSError:
for f in [src, dest]:
try:
is_socket_file = is_socket(f)
except OSError:
# An error has already occurred. Another error here is not
# a problem and we can ignore it.
pass
else:
if is_socket_file:
raise shutil.SpecialFileError("`{f}` is a socket".format(**locals()))
raise
def _copy2_ignoring_special_files(src: str, dest: str) -> None:
"""Copying special files is not supported, but as a convenience to users we
skip errors copying them.
This supports tools that may create e.g. socket files in the project
source directory.
"""
try:
copy2_fixed(src, dest)
except shutil.SpecialFileError as e:
# SpecialFileError may be raised due to either the source or
# destination. If the destination was the cause then we would actually
# care, but since the destination directory is deleted prior to
# copy we ignore all of them assuming it is caused by the source.
logger.warning(
"Ignoring special file error '%s' encountered copying %s to %s.",
str(e),
src,
dest,
)
def _copy_source_tree(source: str, target: str) -> None:
target_abspath = os.path.abspath(target)
target_basename = os.path.basename(target_abspath)
target_dirname = os.path.dirname(target_abspath)
def ignore(d: str, names: List[str]) -> List[str]:
skipped: List[str] = []
if d == source:
# Pulling in those directories can potentially be very slow,
# exclude the following directories if they appear in the top
# level dir (and only it).
# See discussion at https://github.com/pypa/pip/pull/6770
skipped += [".tox", ".nox"]
if os.path.abspath(d) == target_dirname:
# Prevent an infinite recursion if the target is in source.
# This can happen when TMPDIR is set to ${PWD}/...
# and we copy PWD to TMPDIR.
skipped += [target_basename]
return skipped
shutil.copytree(
source,
target,
ignore=ignore,
symlinks=True,
copy_function=_copy2_ignoring_special_files,
)
-324
View File
@@ -1,324 +0,0 @@
import itertools
import os
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Text, Union
import pipenv.vendor.tomlkit as tomlkit
from pipenv.vendor.plette import pipfiles
from pipenv.vendor.pydantic import BaseModel, validator
from ..environment import MYPY_RUNNING
from ..exceptions import RequirementError
from ..utils import is_editable, is_vcs, merge_items
from .common import ReqLibBaseModel
from .project import ProjectFile
from .requirements import Requirement
from .utils import get_url_name, tomlkit_value_to_python
if MYPY_RUNNING:
package_type = Dict[Text, Dict[Text, Union[List[Text], Text]]]
source_type = Dict[Text, Union[Text, bool]]
sources_type = Iterable[source_type]
meta_type = Dict[Text, Union[int, Dict[Text, Text], sources_type]]
lockfile_type = Dict[Text, Union[package_type, meta_type]]
def reorder_source_keys(data):
# type: (tomlkit.toml_document.TOMLDocument) -> tomlkit.toml_document.TOMLDocument
sources = [] # type: sources_type
for source_key in ["source", "sources"]:
sources.extend(data.get(source_key, tomlkit.aot()).value)
new_source_aot = tomlkit.aot()
for entry in sources:
table = tomlkit.table() # type: tomlkit.items.Table
source_entry = PipfileLoader.populate_source(entry.copy())
for key in ["name", "url", "verify_ssl"]:
table.update({key: source_entry[key]})
new_source_aot.append(table)
data["source"] = new_source_aot
if data.get("sources", None):
del data["sources"]
return data
class PipfileLoader(pipfiles.Pipfile):
@classmethod
def validate(cls, data):
# type: (tomlkit.toml_document.TOMLDocument) -> None
for key, klass in pipfiles.PIPFILE_SECTIONS.items():
if key not in data or key == "sources":
continue
try:
klass.validate(data[key])
except Exception:
pass
@classmethod
def ensure_package_sections(cls, data):
# type: (tomlkit.toml_document.TOMLDocument[Text, Any]) -> tomlkit.toml_document.TOMLDocument[Text, Any]
"""Ensure that all pipfile package sections are present in the given
toml document.
:param :class:`~tomlkit.toml_document.TOMLDocument` data: The toml document to
ensure package sections are present on
:return: The updated toml document, ensuring ``packages`` and ``dev-packages``
sections are present
:rtype: :class:`~tomlkit.toml_document.TOMLDocument`
"""
package_keys = (
k for k in pipfiles.PIPFILE_SECTIONS.keys() if k.endswith("packages")
)
for key in package_keys:
if key not in data:
data.update({key: tomlkit.table()})
return data
@classmethod
def populate_source(cls, source):
"""Derive missing values of source from the existing fields."""
# Only URL pararemter is mandatory, let the KeyError be thrown.
if "name" not in source:
source["name"] = get_url_name(source["url"])
if "verify_ssl" not in source:
source["verify_ssl"] = "https://" in source["url"]
if not isinstance(source["verify_ssl"], bool):
source["verify_ssl"] = str(source["verify_ssl"]).lower() == "true"
return source
@classmethod
def load(cls, f, encoding=None):
# type: (Any, Text) -> PipfileLoader
content = f.read()
if encoding is not None:
content = content.decode(encoding)
_data = tomlkit.loads(content)
should_reload = "source" not in _data
_data = reorder_source_keys(_data)
if should_reload:
if "sources" in _data:
content = tomlkit.dumps(_data)
else:
# HACK: There is no good way to prepend a section to an existing
# TOML document, but there's no good way to copy non-structural
# content from one TOML document to another either. Modify the
# TOML content directly, and load the new in-memory document.
sep = "" if content.startswith("\n") else "\n"
content = pipfiles.DEFAULT_SOURCE_TOML + sep + content
data = tomlkit.loads(content)
data = cls.ensure_package_sections(data)
instance = cls(data)
instance._data = dict(instance._data)
return instance
def __contains__(self, key):
# type: (Text) -> bool
if key not in self._data:
package_keys = self._data.get("packages", {}).keys()
dev_package_keys = self._data.get("dev-packages", {}).keys()
return any(key in pkg_list for pkg_list in (package_keys, dev_package_keys))
return True
def __getattribute__(self, key):
# type: (Text) -> Any
if key == "source":
return self._data[key]
return super(PipfileLoader, self).__getattribute__(key)
class Pipfile(ReqLibBaseModel):
path: Path
projectfile: ProjectFile
pipfile: Optional[PipfileLoader]
_pyproject: Optional[tomlkit.TOMLDocument] = tomlkit.document()
build_system: Optional[Dict] = dict()
_requirements: Optional[List] = list()
_dev_requirements: Optional[List] = list()
class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)
@validator("path", pre=True, always=True)
def _get_path(cls, v):
return v or Path(os.curdir).absolute()
@validator("projectfile", pre=True, always=True)
def _get_projectfile(cls, v, values):
return v or cls.load_projectfile(os.curdir, create=False)
@validator("pipfile", pre=True, always=True)
def _get_pipfile(cls, v, values):
return v or values["projectfile"].model
@property
def root(self):
return self.path.parent
@property
def extended_keys(self):
return [
k
for k in itertools.product(
("packages", "dev-packages"), ("", "vcs", "editable")
)
]
def get_deps(self, dev=False, only=True):
deps = {} # type: Dict[Text, Dict[Text, Union[List[Text], Text]]]
if dev:
deps.update(dict(self.pipfile._data.get("dev-packages", {})))
if only:
return deps
return tomlkit_value_to_python(
merge_items([deps, dict(self.pipfile._data.get("packages", {}))])
)
def get(self, k):
return self.__getitem__(k)
def __contains__(self, k):
check_pipfile = k in self.extended_keys or self.pipfile.__contains__(k)
if check_pipfile:
return True
return False
def __getitem__(self, k, *args, **kwargs):
retval = None
pipfile = self.pipfile
section = None
pkg_type = None
try:
retval = pipfile[k]
except KeyError:
if "-" in k:
section, _, pkg_type = k.rpartition("-")
vals = getattr(pipfile.get(section, {}), "_data", {})
vals = tomlkit_value_to_python(vals)
if pkg_type == "vcs":
retval = {k: v for k, v in vals.items() if is_vcs(v)}
elif pkg_type == "editable":
retval = {k: v for k, v in vals.items() if is_editable(v)}
if retval is None:
raise
else:
retval = getattr(retval, "_data", retval)
return retval
def __getattr__(self, k, *args, **kwargs):
pipfile = self.pipfile
try:
retval = super(Pipfile).__getattribute__(k)
except AttributeError:
retval = getattr(pipfile, k, None)
return retval
@property
def requires_python(self):
# type: () -> bool
return getattr(
self.pipfile.requires,
"python_version",
getattr(self.pipfile.requires, "python_full_version", None),
)
@property
def allow_prereleases(self):
# type: () -> bool
return self.pipfile.get("pipenv", {}).get("allow_prereleases", False)
@classmethod
def read_projectfile(cls, path):
# type: (Text) -> ProjectFile
"""Read the specified project file and provide an interface for
writing/updating.
:param Text path: Path to the target file.
:return: A project file with the model and location for interaction
:rtype: :class:`~requirementslib.models.project.ProjectFile`
"""
pf = ProjectFile.read(path, PipfileLoader, invalid_ok=True)
return pf
@classmethod
def load_projectfile(cls, path, create=False):
# type: (Text, bool) -> ProjectFile
"""Given a path, load or create the necessary pipfile.
:param Text path: Path to the project root or pipfile
:param bool create: Whether to create the pipfile if not found, defaults to True
:raises OSError: Thrown if the project root directory doesn't exist
:raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False``
:return: A project file instance for the supplied project
:rtype: :class:`~requirementslib.models.project.ProjectFile`
"""
if not path:
raise RuntimeError("Must pass a path to classmethod 'Pipfile.load'")
if not isinstance(path, Path):
path = Path(path).absolute()
pipfile_path = path if path.is_file() else path.joinpath("Pipfile")
project_path = pipfile_path.parent
if not project_path.exists():
raise FileNotFoundError("%s is not a valid project path!" % path)
elif not pipfile_path.exists() or not pipfile_path.is_file():
if not create:
raise RequirementError("%s is not a valid Pipfile" % pipfile_path)
return cls.read_projectfile(pipfile_path.as_posix())
@classmethod
def load(cls, path, create=False):
# type: (Text, bool) -> Pipfile
"""Given a path, load or create the necessary pipfile.
:param Text path: Path to the project root or pipfile
:param bool create: Whether to create the pipfile if not found, defaults to True
:raises OSError: Thrown if the project root directory doesn't exist
:raises FileNotFoundError: Thrown if the pipfile doesn't exist and ``create=False``
:return: A pipfile instance pointing at the supplied project
:rtype:: class:`~requirementslib.models.pipfile.Pipfile`
"""
projectfile = cls.load_projectfile(path, create=create)
pipfile = projectfile.model
creation_args = {
"projectfile": projectfile,
"pipfile": pipfile,
"path": Path(projectfile.location),
}
return cls(**creation_args)
@property
def dev_packages(self):
# type: () -> List[Requirement]
return self.dev_requirements
@property
def packages(self):
# type: () -> List[Requirement]
return self.requirements
@property
def dev_requirements(self):
# type: () -> List[Requirement]
if not self._dev_requirements:
packages = tomlkit_value_to_python(self.pipfile.get("dev-packages", {}))
self._dev_requirements = [
Requirement.from_pipfile(k, v)
for k, v in packages.items()
if v is not None
]
return self._dev_requirements
@property
def requirements(self):
# type: () -> List[Requirement]
if not self._requirements:
packages = tomlkit_value_to_python(self.pipfile.get("packages", {}))
self._requirements = [
Requirement.from_pipfile(k, v)
for k, v in packages.items()
if v is not None
]
return self._requirements
-69
View File
@@ -1,69 +0,0 @@
import collections
import io
import os
from typing import Any, Optional
from pipenv.patched.pip._vendor.packaging.markers import Marker
from pipenv.vendor.pydantic import BaseModel, Field
SectionDifference = collections.namedtuple("SectionDifference", ["inthis", "inthat"])
FileDifference = collections.namedtuple("FileDifference", ["default", "develop"])
def _are_pipfile_entries_equal(a, b):
a = {k: v for k, v in a.items() if k not in ("markers", "hashes", "hash")}
b = {k: v for k, v in b.items() if k not in ("markers", "hashes", "hash")}
if a != b:
return False
try:
marker_eval_a = Marker(a["markers"]).evaluate()
except (AttributeError, KeyError, TypeError, ValueError):
marker_eval_a = True
try:
marker_eval_b = Marker(b["markers"]).evaluate()
except (AttributeError, KeyError, TypeError, ValueError):
marker_eval_b = True
return marker_eval_a == marker_eval_b
DEFAULT_NEWLINES = "\n"
def preferred_newlines(f):
if isinstance(f.newlines, str):
return f.newlines
return DEFAULT_NEWLINES
class ProjectFile(BaseModel):
location: str
line_ending: str
model: Optional[Any] = Field(default_factory=lambda: dict())
@classmethod
def read(cls, location: str, model_cls, invalid_ok: bool = False) -> "ProjectFile":
if not os.path.exists(location) and not invalid_ok:
raise FileNotFoundError(location)
try:
with io.open(location, encoding="utf-8") as f:
model = model_cls.load(f)
line_ending = preferred_newlines(f)
except Exception:
if not invalid_ok:
raise
model = {}
line_ending = DEFAULT_NEWLINES
return cls(location=location, line_ending=line_ending, model=model)
def write(self) -> None:
kwargs = {"encoding": "utf-8", "newline": self.line_ending}
with io.open(self.location, "w", **kwargs) as f:
if self.model:
self.model.dump(f)
def dumps(self) -> str:
if self.model:
strio = io.StringIO()
self.model.dump(strio)
return strio.getvalue()
return ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-484
View File
@@ -1,484 +0,0 @@
from typing import Dict, Optional, Text, Tuple, TypeVar, Union
from urllib.parse import quote
from urllib.parse import unquote as url_unquote
from urllib.parse import unquote_plus
from pipenv.patched.pip._internal.models.link import Link
from pipenv.patched.pip._internal.req.constructors import _strip_extras
from pipenv.patched.pip._vendor.urllib3.util import parse_url as urllib3_parse
from pipenv.patched.pip._vendor.urllib3.util.url import Url
from pipenv.vendor.pydantic import Field
from ..environment import MYPY_RUNNING
from ..utils import is_installable_file
from .common import ReqLibBaseModel
from .utils import DIRECT_URL_RE, extras_to_string, parse_extras_str, split_ref_from_uri
if MYPY_RUNNING:
_T = TypeVar("_T")
STRING_TYPE = Union[bytes, str, Text]
S = TypeVar("S", bytes, str, Text)
def _get_parsed_url(url) -> Url:
"""This is a stand-in function for `urllib3.util.parse_url`
The original function doesn't handle special characters very well, this simply splits
out the authentication section, creates the parsed url, then puts the authentication
section back in, bypassing validation.
:return: The new, parsed URL object
:rtype: :class:`~urllib3.util.url.Url`
"""
try:
parsed = urllib3_parse(url)
except ValueError:
scheme, _, url = url.partition("://")
auth, _, url = url.rpartition("@")
url = "{scheme}://{url}".format(scheme=scheme, url=url)
parsed = urllib3_parse(url)._replace(auth=auth)
if parsed.auth:
return parsed._replace(auth=url_unquote(parsed.auth))
return parsed
class URI(ReqLibBaseModel):
host: Optional[str] = Field(...)
scheme: Optional[str] = Field(
"https", description="The URI Scheme, e.g. `salesforce`"
)
port: Optional[int] = Field(
None, description="The numeric port of the url if specified"
)
path: Optional[str] = Field("", description="The url path, e.g. `/path/to/endpoint`")
query: Optional[str] = Field(
"", description="Query parameters, e.g. `?variable=value...`"
)
fragment: Optional[str] = Field(
"", description="URL Fragments, e.g. `#fragment=value`"
)
subdirectory: Optional[str] = Field(
"", description="Subdirectory fragment, e.g. `&subdirectory=blah...`"
)
ref: Optional[str] = Field("", description="VCS ref this URI points at, if available")
username: Optional[str] = Field(
"", description="The username if provided, parsed from `user:password@hostname`"
)
password: Optional[str] = Field(
"", description="Password parsed from `user:password@hostname`", repr=False
)
query_dict: Optional[Dict] = Field(default_factory=dict)
name: Optional[str] = Field(
"",
description="The name of the specified package in case it is a VCS URI with an egg fragment",
)
extras: Optional[Tuple] = Field(default_factory=tuple)
is_direct_url: Optional[bool] = Field(False)
is_implicit_ssh: Optional[bool] = Field(False)
auth: Optional[str] = None
_fragment_dict: Optional[Dict] = Field(default_factory=dict)
_username_is_quoted: Optional[bool] = False
_password_is_quoted: Optional[bool] = False
class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
# keep_untouched = (cached_property,)
def __init__(self, **data):
super().__init__(**data)
self._parse_auth()
self._parse_query()
self._parse_fragment()
def _parse_query(self) -> None:
query = self.query if self.query is not None else ""
query_dict = dict()
queries = query.split("&")
query_items = []
subdirectory = self.subdirectory if self.subdirectory else None
for q in queries:
key, _, val = q.partition("=")
val = unquote_plus(val)
if key == "subdirectory" and not subdirectory:
subdirectory = val
else:
query_items.append((key, val))
query_dict.update(query_items)
self.query_dict = query_dict
self.subdirectory = subdirectory
self.query = query
def _parse_fragment(self) -> None:
subdirectory = self.subdirectory if self.subdirectory else ""
fragment = self.fragment if self.fragment else ""
if self.fragment is None:
return self
fragments = self.fragment.split("&")
fragment_items = {}
name = self.name if self.name else ""
extras = self.extras
for q in fragments:
key, _, val = q.partition("=")
val = unquote_plus(val)
fragment_items[key] = val
if key == "egg":
from .utils import parse_extras_str
name, stripped_extras = _strip_extras(val)
if stripped_extras:
extras = tuple(parse_extras_str(stripped_extras))
elif key == "subdirectory":
subdirectory = val
self.name = name
self.extras = extras
self.subdirectory = subdirectory
self.fragment = fragment
self._fragment_dict = fragment_items
def _parse_auth(self) -> None:
if self.auth:
username, _, password = self.auth.partition(":")
username_is_quoted, password_is_quoted = False, False
quoted_username, quoted_password = "", ""
if password:
quoted_password = quote(password)
password_is_quoted = quoted_password != password
if username:
quoted_username = quote(username)
username_is_quoted = quoted_username != username
self.username = quoted_username
self.password = quoted_password
self._username_is_quoted = username_is_quoted
self._password_is_quoted = password_is_quoted
def get_password(self, unquote=False, include_token=True) -> str:
password = self.password if self.password else ""
if password and unquote and self._password_is_quoted:
password = url_unquote(password)
return password
def get_username(self, unquote=False) -> str:
username = self.username if self.username else ""
if username and unquote and self._username_is_quoted:
username = url_unquote(username)
return username
@staticmethod
def parse_subdirectory(url_part):
# type: (str) -> Tuple[str, Optional[str]]
subdir = None
if "&subdirectory" in url_part:
url_part, _, subdir = url_part.rpartition("&")
if "#egg=" not in url_part:
subdir = "#{0}".format(subdir.strip())
else:
subdir = "&{0}".format(subdir.strip())
return url_part.strip(), subdir
@classmethod
def get_parsed_url(cls, url):
# if there is a "#" in the auth section, this could break url parsing
maybe_auth = None
parsed_url = _get_parsed_url(url)
if "@" in url and "#" in url:
scheme = "{0}://".format(parsed_url.scheme)
if parsed_url.scheme == "file":
scheme = "{0}/".format(scheme)
url_without_scheme = url.replace(scheme, "")
maybe_auth, _, maybe_url = url_without_scheme.partition("@")
if "#" in maybe_auth and (not parsed_url.host or "." not in parsed_url.host):
new_parsed_url = _get_parsed_url("{0}{1}".format(scheme, maybe_url))
new_parsed_url = new_parsed_url._replace(auth=maybe_auth)
return new_parsed_url
return parsed_url
@classmethod
def parse(cls, url) -> "URI":
is_direct_url = False
name_with_extras = None
is_implicit_ssh = url.strip().startswith("git+git@")
if is_implicit_ssh:
from ..utils import add_ssh_scheme_to_git_uri
url = add_ssh_scheme_to_git_uri(url)
direct_match = DIRECT_URL_RE.match(url)
if direct_match is not None:
is_direct_url = True
name_with_extras, _, url = url.partition("@")
name_with_extras = name_with_extras.strip()
url, ref = split_ref_from_uri(url.strip())
if "file:/" in url and "file:///" not in url:
url = url.replace("file:/", "file:///")
parsed = cls.get_parsed_url(url)
# if there is a "#" in the auth section, this could break url parsing
if not (parsed.scheme and parsed.host):
# check if this is a file uri
if not (
parsed.scheme
and parsed.path
and (parsed.scheme == "file" or parsed.scheme.endswith("+file"))
):
raise ValueError("Failed parsing URL {0!r} - Not a valid url".format(url))
parsed_dict = dict(parsed._asdict()).copy()
parsed_dict["is_direct_url"] = is_direct_url
parsed_dict["is_implicit_ssh"] = is_implicit_ssh
parsed_dict.update(
**update_url_name_and_fragment(name_with_extras, ref, parsed_dict)
) # type: ignore
return cls(**parsed_dict)
def to_string(
self,
escape_password=True, # type: bool
unquote=True, # type: bool
direct=None, # type: Optional[bool]
strip_ssh=False, # type: bool
strip_ref=False, # type: bool
strip_name=False, # type: bool
strip_subdir=False, # type: bool
):
# type: (...) -> str
"""Converts the current URI to a string, unquoting or escaping the
password as needed.
:param escape_password: Whether to replace password with ``----``, default True
:param escape_password: bool, optional
:param unquote: Whether to unquote url-escapes in the password, default False
:param unquote: bool, optional
:param bool direct: Whether to format as a direct URL
:param bool strip_ssh: Whether to strip the SSH scheme from the url (git only)
:param bool strip_ref: Whether to drop the VCS ref (if present)
:param bool strip_name: Whether to drop the name and extras (if present)
:param bool strip_subdir: Whether to drop the subdirectory (if present)
:return: The reconstructed string representing the URI
:rtype: str
"""
if direct is None:
direct = self.is_direct_url
if escape_password:
password = "----" if self.password else ""
if password:
username = self.get_username(unquote=unquote)
elif self.username:
username = "----"
else:
username = ""
else:
password = self.get_password(unquote=unquote)
username = self.get_username(unquote=unquote)
auth = ""
if username:
if password:
auth = "{username}:{password}@".format(
password=password, username=username
)
else:
auth = "{username}@".format(username=username)
query = ""
if self.query:
query = "{query}?{self.query}".format(query=query, self=self)
subdir_prefix = "#"
if not direct:
if self.name and not strip_name:
fragment = "#egg={self.name_with_extras}".format(self=self)
subdir_prefix = "&"
elif not strip_name and (
self.extras and self.scheme and self.scheme.startswith("file")
):
from .utils import extras_to_string
fragment = extras_to_string(self.extras)
else:
fragment = ""
query = "{query}{fragment}".format(query=query, fragment=fragment)
if self.subdirectory and not strip_subdir:
query = "{query}{subdir_prefix}subdirectory={self.subdirectory}".format(
query=query, subdir_prefix=subdir_prefix, self=self
)
host_port_path = self.get_host_port_path(strip_ref=strip_ref)
url = "{self.scheme}://{auth}{host_port_path}{query}".format(
self=self, auth=auth, host_port_path=host_port_path, query=query
)
if strip_ssh:
from ..utils import strip_ssh_from_git_uri
url = strip_ssh_from_git_uri(url)
if self.name and direct and not strip_name:
return "{self.name_with_extras}@ {url}".format(self=self, url=url)
return url
def get_host_port_path(self, strip_ref=False):
# type: (bool) -> str
host = self.host if self.host else ""
if self.port is not None:
host = "{host}:{self.port!s}".format(host=host, self=self)
path = "{self.path}".format(self=self) if self.path else ""
if self.ref and not strip_ref:
path = "{path}@{self.ref}".format(path=path, self=self)
return "{host}{path}".format(host=host, path=path)
@property
def hidden_auth(self):
# type: () -> str
auth = ""
if self.username and self.password:
password = "****"
username = self.get_username(unquote=True)
auth = "{username}:{password}".format(username=username, password=password)
elif self.username and not self.password:
auth = "****"
return auth
@property
def name_with_extras(self):
# type: () -> str
from .utils import extras_to_string
if not self.name:
return ""
extras = extras_to_string(self.extras)
return "{self.name}{extras}".format(self=self, extras=extras)
@property
def as_link(self):
# type: () -> Link
link = Link(self.to_string(escape_password=False, strip_ssh=False, direct=False))
return link
@property
def bare_url(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_name=True,
strip_ref=True,
strip_subdir=True,
)
@property
def url_without_fragment_or_ref(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_name=True,
strip_ref=True,
)
@property
def url_without_fragment(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_name=True,
)
@property
def url_without_ref(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
strip_ref=True,
)
@property
def base_url(self):
# type: () -> str
return self.to_string(
escape_password=False,
strip_ssh=self.is_implicit_ssh,
direct=False,
unquote=False,
)
@property
def full_url(self):
# type: () -> str
return self.to_string(escape_password=False, strip_ssh=False, direct=False)
@property
def secret(self):
# type: () -> str
return self.full_url
@property
def safe_string(self):
# type: () -> str
return self.to_string(escape_password=True, unquote=True)
@property
def unsafe_string(self):
# type: () -> str
return self.to_string(escape_password=False, unquote=True)
@property
def uri_escape(self):
# type: () -> str
return self.to_string(escape_password=False, unquote=False)
@property
def is_installable(self):
# type: () -> bool
return self.is_file_url and is_installable_file(self.bare_url)
@property
def is_vcs(self):
# type: () -> bool
from ..utils import VCS_SCHEMES
return self.scheme in VCS_SCHEMES
@property
def is_file_url(self):
# type: () -> bool
return all([self.scheme, self.scheme == "file"])
def __str__(self):
# type: () -> str
return self.to_string(escape_password=True, unquote=True)
def update_url_name_and_fragment(name_with_extras, ref, parsed_dict):
# type: (Optional[str], Optional[str], Dict[str, Optional[str]]) -> Dict[str, Optional[str]]
if name_with_extras:
fragment = "" # type: Optional[str]
parsed_extras = ()
name, extras = _strip_extras(name_with_extras)
if extras:
parsed_extras = parsed_extras + tuple(parse_extras_str(extras))
if parsed_dict["fragment"] is not None:
fragment = "{0}".format(parsed_dict["fragment"])
if fragment.startswith("egg="):
_, _, fragment_part = fragment.partition("=")
fragment_name, fragment_extras = _strip_extras(fragment_part)
name = name if name else fragment_name
if fragment_extras:
parsed_extras = parsed_extras + tuple(
parse_extras_str(fragment_extras)
)
name_with_extras = "{0}{1}".format(name, extras_to_string(parsed_extras))
elif (
parsed_dict.get("path") is not None and "&subdirectory" in parsed_dict["path"]
):
path, fragment = URI.parse_subdirectory(parsed_dict["path"]) # type: ignore
parsed_dict["path"] = path
elif ref is not None and "&subdirectory" in ref:
ref, fragment = URI.parse_subdirectory(ref)
parsed_dict["name"] = name
parsed_dict["extras"] = parsed_extras
if ref:
parsed_dict["ref"] = ref.strip()
return parsed_dict
-787
View File
@@ -1,787 +0,0 @@
import os
import re
import string
from functools import lru_cache
from pathlib import Path
from typing import (
Any,
AnyStr,
Dict,
List,
Match,
Optional,
Set,
Text,
Tuple,
TypeVar,
Union,
)
import pipenv.vendor.tomlkit as tomlkit
from pipenv.patched.pip._internal.models.link import Link
from pipenv.patched.pip._internal.req.constructors import install_req_from_line
from pipenv.patched.pip._internal.utils._jaraco_text import drop_comment, join_continuation, yield_lines
from pipenv.patched.pip._vendor.packaging.markers import InvalidMarker, Marker, Op, Value, Variable
from pipenv.patched.pip._vendor.packaging.requirements import Requirement as PackagingRequirement
from pipenv.patched.pip._vendor.packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
from pipenv.patched.pip._vendor.pkg_resources import Requirement, get_distribution, safe_name
from pipenv.patched.pip._vendor.urllib3 import util as urllib3_util
from pipenv.patched.pip._vendor.urllib3.util import parse_url as urllib3_parse
from pipenv.vendor.plette.models import Package, PackageCollection
from pipenv.vendor.tomlkit.container import Container
from pipenv.vendor.tomlkit.items import AoT, Array, Bool, InlineTable, Item, String, Table
from ..environment import MYPY_RUNNING
from ..fileutils import is_valid_url
from ..utils import VCS_LIST, is_star
if MYPY_RUNNING:
from pipenv.patched.pip._vendor.packaging.markers import Marker as PkgResourcesMarker
from pipenv.patched.pip._vendor.packaging.markers import Op as PkgResourcesOp
from pipenv.patched.pip._vendor.packaging.markers import Value as PkgResourcesValue
from pipenv.patched.pip._vendor.packaging.markers import Variable as PkgResourcesVariable
from pipenv.patched.pip._vendor.urllib3.util.url import Url
_T = TypeVar("_T")
TMarker = Union[Marker, PkgResourcesMarker]
TVariable = TypeVar("TVariable", PkgResourcesVariable, Variable)
TValue = TypeVar("TValue", PkgResourcesValue, Value)
TOp = TypeVar("TOp", PkgResourcesOp, Op)
MarkerTuple = Tuple[TVariable, TOp, TValue]
TRequirement = Union[PackagingRequirement, Requirement]
STRING_TYPE = Union[bytes, str, Text]
TOML_DICT_TYPES = Union[Container, Package, PackageCollection, Table, InlineTable]
S = TypeVar("S", bytes, str, Text)
TOML_DICT_OBJECTS = (Container, Package, Table, InlineTable, PackageCollection)
TOML_DICT_NAMES = [o.__class__.__name__ for o in TOML_DICT_OBJECTS]
HASH_STRING = " --hash={0}"
ALPHA_NUMERIC = r"[{0}{1}]".format(string.ascii_letters, string.digits)
PUNCTUATION = r"[\-_\.]"
ALPHANUM_PUNCTUATION = r"[{0}{1}\-_\.]".format(string.ascii_letters, string.digits)
NAME = r"{0}+{1}*{2}".format(ALPHANUM_PUNCTUATION, PUNCTUATION, ALPHA_NUMERIC)
REF = r"[{0}{1}\-\_\./]".format(string.ascii_letters, string.digits)
EXTRAS = r"(?P<extras>\[{0}(?:,{0})*\])".format(NAME)
NAME_WITH_EXTRAS = r"(?P<name>{0}){1}?".format(NAME, EXTRAS)
NAME_RE = re.compile(NAME_WITH_EXTRAS)
SUBDIR_RE = r"(?:[&#]subdirectory=(?P<subdirectory>.*))"
URL_NAME = r"(?:#egg={0})".format(NAME_WITH_EXTRAS)
REF_RE = r"(?:@(?P<ref>{0}+)?)".format(REF)
PATH_RE = r"(?P<pathsep>[:/])(?P<path>[^ @]+){0}?".format(REF_RE)
PASS_RE = r"(?:(?<=:)(?P<password>[^ ]+))"
AUTH_RE = r"(?:(?P<username>[^ ]+)[:@]{0}?@)".format(PASS_RE)
HOST_RE = r"(?:{0}?(?P<host>[^ ]+?\.?{1}+(?P<port>:\d+)?))?".format(
AUTH_RE, ALPHA_NUMERIC
)
URL = r"(?P<scheme>[^ ]+://){0}{1}".format(HOST_RE, PATH_RE)
URL_RE = re.compile(r"{0}(?:{1}?{2}?)?".format(URL, URL_NAME, SUBDIR_RE))
DIRECT_URL_RE = re.compile(r"{0}\s?@\s?{1}".format(NAME_WITH_EXTRAS, URL))
def filter_none(k, v) -> bool:
if v:
return True
return False
def filter_dict(dict_) -> Dict[AnyStr, Any]:
return {k: v for k, v in dict_.items() if filter_none(k, v)}
def create_link(link):
# type: (AnyStr) -> Link
if not isinstance(link, str):
raise TypeError("must provide a string to instantiate a new link")
return Link(link)
def tomlkit_value_to_python(toml_value):
# type: (Union[Array, AoT, TOML_DICT_TYPES, Item]) -> Union[List, Dict]
value_type = type(toml_value).__name__
if (
isinstance(toml_value, TOML_DICT_OBJECTS + (dict,))
or value_type in TOML_DICT_NAMES
):
return tomlkit_dict_to_python(toml_value)
elif isinstance(toml_value, AoT) or value_type == "AoT":
return [tomlkit_value_to_python(val) for val in toml_value._body]
elif isinstance(toml_value, Array) or value_type == "Array":
return [tomlkit_value_to_python(val) for val in list(toml_value)]
elif isinstance(toml_value, String) or value_type == "String":
return "{0!s}".format(toml_value)
elif isinstance(toml_value, Bool) or value_type == "Bool":
return toml_value.value
elif isinstance(toml_value, Item):
return toml_value.value
return toml_value
def tomlkit_dict_to_python(toml_dict):
# type: (TOML_DICT_TYPES) -> Dict
value_type = type(toml_dict).__name__
if toml_dict is None:
raise TypeError("Invalid type NoneType when converting toml dict to python")
converted = None # type: Optional[Dict]
if isinstance(toml_dict, (InlineTable, Table)) or value_type in (
"InlineTable",
"Table",
):
converted = toml_dict.value
elif isinstance(toml_dict, (Package, PackageCollection)) or value_type in (
"Package, PackageCollection"
):
converted = toml_dict._data
if isinstance(converted, Container) or type(converted).__name__ == "Container":
converted = converted.value
elif isinstance(toml_dict, Container) or value_type == "Container":
converted = toml_dict.value
elif isinstance(toml_dict, dict):
converted = toml_dict.copy()
else:
raise TypeError(
"Invalid type for conversion: expected Container, Dict, or Table, "
"got {0!r}".format(toml_dict)
)
if isinstance(converted, dict):
return {k: tomlkit_value_to_python(v) for k, v in converted.items()}
elif isinstance(converted, (TOML_DICT_OBJECTS)) or value_type in TOML_DICT_NAMES:
return tomlkit_dict_to_python(converted)
return converted
def get_url_name(url):
# type: (AnyStr) -> AnyStr
"""Given a url, derive an appropriate name to use in a pipfile.
:param str url: A url to derive a string from
:returns: The name of the corresponding pipfile entry
:rtype: Text
"""
if not isinstance(url, str):
raise TypeError("Expected a string, got {0!r}".format(url))
return urllib3_util.parse_url(url).host
class HashableRequirement(Requirement):
def __hash__(self):
specifier_hash = hash(tuple((str(s),) for s in self.specifier))
return hash(
(
self.url,
specifier_hash,
frozenset(self.extras),
str(self.marker) if self.marker else None,
)
)
@staticmethod
def parse(s):
(req,) = map(
HashableRequirement, join_continuation(map(drop_comment, yield_lines(s)))
)
return req
def init_requirement(name):
if not isinstance(name, str):
raise TypeError("must supply a name to generate a requirement")
req = HashableRequirement.parse(name)
req.vcs = None
req.local_file = None
req.revision = None
req.path = None
return req
def convert_to_hashable_requirement(req: Requirement) -> Optional[HashableRequirement]:
if req is None:
return None
hashable_req = HashableRequirement(str(req))
hashable_req.extras = req.extras
hashable_req.marker = req.marker
hashable_req.url = req.url
return hashable_req
def extras_to_string(extras) -> str:
"""Turn a list of extras into a string."""
if isinstance(extras, str):
if extras.startswith("["):
return extras
else:
extras = [extras]
if not extras:
return ""
return "[{0}]".format(",".join(sorted(set(extras))))
def parse_extras_str(extras_str):
"""Turn a string of extras into a parsed extras list.
:param str extras_str: An extras string
:return: A sorted list of extras
:rtype: List[str]
"""
extras = Requirement.parse("fakepkg{0}".format(extras_to_string(extras_str))).extras
return sorted(dict.fromkeys([extra.lower() for extra in extras]))
def parse_extras_from_line(installable_line):
extras = []
# Extract the part within square brackets, if any
start = installable_line.find("[")
end = installable_line.find("]")
if start != -1 and end != -1 and start < end:
extras_str = installable_line[start + 1 : end]
extras = extras_str.split(",")
return extras
def specs_to_string(specs):
# type: (List[Union[STRING_TYPE, Specifier]]) -> AnyStr
"""Turn a list of specifier tuples into a string.
:param List[Union[Specifier, str]] specs: a list of specifiers to format
:return: A string of specifiers
:rtype: str
"""
if specs:
if isinstance(specs, str):
return specs
try:
extras = ",".join(["".join(spec) for spec in specs])
except TypeError:
extras = ",".join(["".join(spec._spec) for spec in specs]) # type: ignore
return extras
return ""
def build_vcs_uri(
vcs,
uri,
name=None,
ref=None,
subdirectory=None,
extras=None,
):
# type: (...) -> STRING_TYPE
if extras is None:
extras = []
vcs_start = ""
if vcs is not None:
vcs_start = "{0}+".format(vcs)
if not uri.startswith(vcs_start):
uri = "{0}{1}".format(vcs_start, uri)
if ref:
uri = "{0}@{1}".format(uri, ref)
if name:
uri = "{0}#egg={1}".format(uri, name)
if extras:
extras_string = extras_to_string(extras)
uri = "{0}{1}".format(uri, extras_string)
if subdirectory:
uri = "{0}&subdirectory={1}".format(uri, subdirectory)
return uri
def _get_parsed_url(url):
# type: (S) -> Url
"""This is a stand-in function for `urllib3.util.parse_url`
The original function doesn't handle special characters very well, this simply splits
out the authentication section, creates the parsed url, then puts the authentication
section back in, bypassing validation.
:return: The new, parsed URL object
:rtype: :class:`~urllib3.util.url.Url`
"""
try:
parsed = urllib3_parse(url)
except ValueError:
scheme, _, url = url.partition("://")
auth, _, url = url.rpartition("@")
url = "{scheme}://{url}".format(scheme=scheme, url=url)
parsed = urllib3_parse(url)._replace(auth=auth)
return parsed
def convert_direct_url_to_url(direct_url):
# type: (AnyStr) -> AnyStr
"""Converts direct URLs to standard, link-style URLs.
Given a direct url as defined by *PEP 508*, convert to a :class:`Link`
compatible URL by moving the name and extras into an **egg_fragment**.
:param str direct_url: A pep-508 compliant direct url.
:return: A reformatted URL for use with Link objects and :class:`InstallRequirement` objects.
:rtype: AnyStr
"""
direct_match = DIRECT_URL_RE.match(direct_url) # type: Optional[Match]
if direct_match is None:
url_match = URL_RE.match(direct_url)
if url_match or is_valid_url(direct_url):
return direct_url
match_dict = (
{}
) # type: Dict[STRING_TYPE, Union[Tuple[STRING_TYPE, ...], STRING_TYPE]]
if direct_match is not None:
match_dict = direct_match.groupdict() # type: ignore
if not match_dict:
raise ValueError(
"Failed converting value to normal URL, is it a direct URL? {0!r}".format(
direct_url
)
)
url_segments = [match_dict.get(s) for s in ("scheme", "host", "path", "pathsep")]
url = "" # type: STRING_TYPE
url = "".join([s for s in url_segments if s is not None]) # type: ignore
new_url = build_vcs_uri(
None,
url,
ref=match_dict.get("ref"),
name=match_dict.get("name"),
extras=match_dict.get("extras"),
subdirectory=match_dict.get("subdirectory"),
)
return new_url
def get_version(pipfile_entry):
if str(pipfile_entry) == "{}" or is_star(pipfile_entry):
return ""
if hasattr(pipfile_entry, "keys") and "version" in pipfile_entry:
if is_star(pipfile_entry.get("version")):
return ""
version = pipfile_entry.get("version")
if version is None:
version = ""
return version.strip().lstrip("(").rstrip(")")
if isinstance(pipfile_entry, str):
return pipfile_entry.strip().lstrip("(").rstrip(")")
return ""
def strip_extras_markers_from_requirement(req):
# type: (TRequirement) -> TRequirement
"""Strips extras markers from requirement instances.
Given a :class:`~packaging.requirements.Requirement` instance with markers defining
*extra == 'name'*, strip out the extras from the markers and return the cleaned
requirement
:param PackagingRequirement req: A packaging requirement to clean
:return: A cleaned requirement
:rtype: PackagingRequirement
"""
if req is None:
raise TypeError("Must pass in a valid requirement, received {0!r}".format(req))
if getattr(req, "marker", None) is not None:
marker = req.marker # type: TMarker
marker._markers = _strip_extras_markers(marker._markers)
if not marker._markers:
req.marker = None
else:
req.marker = marker
return req
def _strip_extras_markers(marker):
# type: (Union[MarkerTuple, List[Union[MarkerTuple, str]]]) -> List[Union[MarkerTuple, str]]
if marker is None or not isinstance(marker, (list, tuple)):
raise TypeError("Expecting a marker type, received {0!r}".format(marker))
markers_to_remove = []
# iterate forwards and generate a list of indexes to remove first, then reverse the
# list so we can remove the text that normally occurs after (but we will already
# be past it in the loop)
for i, marker_list in enumerate(marker):
if isinstance(marker_list, list):
cleaned = _strip_extras_markers(marker_list)
if not cleaned:
markers_to_remove.append(i)
elif isinstance(marker_list, tuple) and marker_list[0].value == "extra":
markers_to_remove.append(i)
for i in reversed(markers_to_remove):
del marker[i]
if i > 0 and marker[i - 1] == "and":
del marker[i - 1]
return marker
@lru_cache()
def get_setuptools_version():
# type: () -> Optional[STRING_TYPE]
setuptools_dist = get_distribution(Requirement("setuptools"))
return getattr(setuptools_dist, "version", None)
def get_default_pyproject_backend():
# type: () -> STRING_TYPE
st_version = get_setuptools_version()
if st_version is not None:
parsed_st_version = parse_version(st_version)
if parsed_st_version >= parse_version("40.8.0"):
return "setuptools.build_meta:__legacy__"
return "setuptools.build_meta"
def get_pyproject(path: Union[AnyStr, Path]) -> Optional[Dict[str, Union[List[AnyStr], AnyStr]]]:
"""
Given a base path, look for the corresponding ``pyproject.toml`` file
and return its build_requires and build_backend.
:param path: The root path of the project, should be a directory (will be truncated)
:return: A dictionary with build requirements, build backend, and dependencies
"""
if not path:
return
if not isinstance(path, Path):
path = Path(path)
if not path.is_dir():
path = path.parent
pp_toml = path / "pyproject.toml"
# Default values
requires = ["setuptools>=40.8", "wheel"]
backend = get_default_pyproject_backend()
dependencies = []
if pp_toml.exists():
with open(pp_toml, encoding="utf-8") as fh:
pyproject_data = tomlkit.loads(fh.read())
# Extracting build system information
build_system = pyproject_data.get("build-system", None)
if build_system is not None:
requires = build_system.get("requires", requires)
backend = build_system.get("build-backend", backend)
# Extracting project dependencies
project_data = pyproject_data.get("project", None)
if project_data is not None:
dependencies = project_data.get("dependencies", [])
return {"build_requires": requires, "build_backend": backend, "dependencies": dependencies}
def split_markers_from_line(line):
# type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]]
"""Split markers from a dependency."""
quote_chars = ["'", '"']
line_quote = next(
iter(quote for quote in quote_chars if line.startswith(quote)), None
)
if line_quote and line.endswith(line_quote):
line = line.strip(line_quote)
marker_sep = " ; "
markers = None
if marker_sep in line:
line, markers = line.split(marker_sep, 1)
markers = markers.strip() if markers else None
return line, markers
def split_vcs_method_from_uri(uri):
# type: (AnyStr) -> Tuple[Optional[STRING_TYPE], STRING_TYPE]
"""Split a vcs+uri formatted uri into (vcs, uri)"""
vcs_start = "{0}+"
vcs = next(
iter([vcs for vcs in VCS_LIST if uri.startswith(vcs_start.format(vcs))]), None
)
if vcs:
vcs, uri = uri.split("+", 1)
return vcs, uri
def split_ref_from_uri(uri):
# type: (AnyStr) -> Tuple[AnyStr, Optional[AnyStr]]
"""Given a path or URI, check for a ref and split it from the path if it is
present, returning a tuple of the original input and the ref or None.
:param AnyStr uri: The path or URI to split
:returns: A 2-tuple of the path or URI and the ref
:rtype: Tuple[AnyStr, Optional[AnyStr]]
"""
if not isinstance(uri, str):
raise TypeError("Expected a string, received {0!r}".format(uri))
parsed = _get_parsed_url(uri)
path = parsed.path if parsed.path else ""
scheme = parsed.scheme if parsed.scheme else ""
ref = None
schema_is_filelike = scheme in ("", "file")
if (not schema_is_filelike and "@" in path) or (
schema_is_filelike and (re.match("^.*@[^/@]*$", path) or path.count("@") >= 2)
):
path, _, ref = path.rpartition("@")
parsed = parsed._replace(path=path)
return (parsed.url, ref)
def validate_vcs(instance, attr_, value):
if value not in VCS_LIST:
raise ValueError("Invalid vcs {0!r}".format(value))
def validate_path(instance, attr_, value):
if not os.path.exists(value):
raise ValueError("Invalid path {0!r}".format(value))
def validate_specifiers(instance, attr_, value):
if value == "":
return True
try:
SpecifierSet(value)
except (InvalidMarker, InvalidSpecifier):
raise ValueError("Invalid Specifiers {0}".format(value))
def key_from_req(req):
"""Get an all-lowercase version of the requirement's name."""
if hasattr(req, "key"):
# from pkg_resources, such as installed dists for pip-sync
key = req.key
else:
# from packaging, such as install requirements from requirements.txt
key = req.name
key = key.replace("_", "-").lower()
return key
def _requirement_to_str_lowercase_name(requirement):
"""Formats a packaging.requirements.Requirement with a lowercase name.
This is simply a copy of
https://github.com/pypa/packaging/blob/16.8/packaging/requirements.py#L109-L124
modified to lowercase the dependency name.
Previously, we were invoking the original Requirement.__str__ method and
lower-casing the entire result, which would lowercase the name, *and* other,
important stuff that should not be lower-cased (such as the marker). See
this issue for more information: https://github.com/pypa/pipenv/issues/2113.
"""
parts = [requirement.name.lower()]
if requirement.extras:
parts.append("[{0}]".format(",".join(sorted(requirement.extras))))
if requirement.specifier:
parts.append(str(requirement.specifier))
if requirement.url:
parts.append("@ {0}".format(requirement.url))
if requirement.marker:
parts.append(" ; {0}".format(requirement.marker))
return "".join(parts)
def format_requirement(ireq):
"""Formats an `InstallRequirement` instance as a string.
Generic formatter for pretty printing InstallRequirements to the terminal
in a less verbose way than using its `__str__` method.
:param :class:`InstallRequirement` ireq: A pip **InstallRequirement** instance.
:return: A formatted string for prettyprinting
:rtype: str
"""
if ireq.editable:
line = "-e {}".format(ireq.link)
else:
line = _requirement_to_str_lowercase_name(ireq.req)
if str(ireq.req.marker) != str(ireq.markers):
if not ireq.req.marker:
line = "{} ; {}".format(line, ireq.markers)
else:
name, markers = line.split(";", 1)
markers = markers.strip()
line = "{} ; ({}) and ({})".format(name, markers, ireq.markers)
return line
def get_pinned_version(ireq):
"""Get the pinned version of an InstallRequirement.
An InstallRequirement is considered pinned if:
- Is not editable
- It has exactly one specifier
- That specifier is "=="
- The version does not contain a wildcard
Examples:
django==1.8 # pinned
django>1.8 # NOT pinned
django~=1.8 # NOT pinned
django==1.* # NOT pinned
Raises `TypeError` if the input is not a valid InstallRequirement, or
`ValueError` if the InstallRequirement is not pinned.
"""
try:
specifier = ireq.specifier
except AttributeError:
raise TypeError("Expected InstallRequirement, not {}".format(type(ireq).__name__))
if getattr(ireq, "editable", False):
raise ValueError("InstallRequirement is editable")
if not specifier:
raise ValueError("InstallRequirement has no version specification")
if len(specifier._specs) != 1:
raise ValueError("InstallRequirement has multiple specifications")
op, version = next(iter(specifier._specs))._spec
if op not in ("==", "===") or version.endswith(".*"):
raise ValueError("InstallRequirement not pinned (is {0!r})".format(op + version))
return version
def is_pinned_requirement(ireq):
"""Returns whether an InstallRequirement is a "pinned" requirement.
An InstallRequirement is considered pinned if:
- Is not editable
- It has exactly one specifier
- That specifier is "=="
- The version does not contain a wildcard
Examples:
django==1.8 # pinned
django>1.8 # NOT pinned
django~=1.8 # NOT pinned
django==1.* # NOT pinned
"""
try:
get_pinned_version(ireq)
except (TypeError, ValueError):
return False
return True
def as_tuple(ireq):
"""Pulls out the (name: str, version:str, extras:(str)) tuple from the
pinned InstallRequirement."""
if not is_pinned_requirement(ireq):
raise TypeError("Expected a pinned InstallRequirement, got {}".format(ireq))
name = key_from_req(ireq.req)
version = next(iter(ireq.specifier._specs))._spec[1]
extras = tuple(sorted(ireq.extras))
return name, version, extras
def make_install_requirement(
name, version=None, extras=None, markers=None, constraint=False
):
"""Generates an :class:`~pipenv.patched.pip._internal.req.req_install.InstallRequirement`.
Create an InstallRequirement from the supplied metadata.
:param name: The requirement's name.
:type name: str
:param version: The requirement version (must be pinned).
:type version: str.
:param extras: The desired extras.
:type extras: list[str]
:param markers: The desired markers, without a preceding semicolon.
:type markers: str
:param constraint: Whether to flag the requirement as a constraint, defaults to False.
:param constraint: bool, optional
:return: A generated InstallRequirement
:rtype: :class:`~pipenv.patched.pip._internal.req.req_install.InstallRequirement`
"""
requirement_string = "{0}".format(name)
if extras:
# Sort extras for stability
extras_string = "[{}]".format(",".join(sorted(extras)))
requirement_string = "{0}{1}".format(requirement_string, extras_string)
if version:
requirement_string = "{0}=={1}".format(requirement_string, str(version))
if markers:
requirement_string = "{0} ; {1}".format(requirement_string, str(markers))
return install_req_from_line(requirement_string, constraint=constraint)
def normalize_name(pkg) -> str:
"""Given a package name, return its normalized, non-canonicalized form."""
return pkg.replace("_", "-").lower()
def get_name_variants(pkg):
# type: (STRING_TYPE) -> Set[STRING_TYPE]
"""Given a packager name, get the variants of its name for both the
canonicalized and "safe" forms.
:param AnyStr pkg: The package to lookup
:returns: A list of names.
:rtype: Set
"""
if not isinstance(pkg, str):
raise TypeError("must provide a string to derive package names")
pkg = pkg.lower()
names = {safe_name(pkg), canonicalize_name(pkg), pkg.replace("-", "_")}
return names
def expand_env_variables(line):
# type: (AnyStr) -> AnyStr
"""Expand the env vars in a line following pip's standard.
https://pip.pypa.io/en/stable/reference/pip_install/#id10.
Matches environment variable-style values in '${MY_VARIABLE_1}' with
the variable name consisting of only uppercase letters, digits or
the '_'
"""
def replace_with_env(match):
value = os.getenv(match.group(1))
return value if value else match.group()
return re.sub(r"\$\{([A-Z0-9_]+)\}", replace_with_env, line)
def tuple_to_dict(input_tuple):
result_dict = {}
i = 0
while i < len(input_tuple):
key = input_tuple[i]
i += 1
if i < len(input_tuple):
if isinstance(input_tuple[i], tuple):
value = tuple_to_dict(input_tuple[i])
i += 1
else:
value = input_tuple[i]
i += 1
result_dict[key] = value
return result_dict
-104
View File
@@ -1,104 +0,0 @@
import importlib
import os
import sys
from typing import Any, Optional, Tuple
from pipenv.patched.pip._internal.vcs.versioncontrol import VcsSupport
from pipenv.patched.pip._vendor.pyparsing.core import cached_property
from pipenv.vendor.pydantic import Field
from .common import ReqLibBaseModel
from .url import URI
class VCSRepository(ReqLibBaseModel):
url: str
name: str
checkout_directory: str
vcs_type: str
parsed_url: Optional[URI] = Field(default_factory=None)
subdirectory: Optional[str] = None
commit_sha: Optional[str] = None
ref: Optional[str] = None
repo_backend: Any = None
clone_log: Optional[str] = None
DEFAULT_RUN_ARGS: Optional[Any] = None
class Config:
validate_assignment = True
arbitrary_types_allowed = True
allow_mutation = True
include_private_attributes = True
keep_untouched = (cached_property,)
def __init__(self, **data):
super().__init__(**data)
self.parsed_url = self.get_parsed_url()
self.repo_backend = self.get_repo_backend()
def get_parsed_url(self) -> URI:
return URI.parse(self.url)
def get_repo_backend(self):
if self.DEFAULT_RUN_ARGS is None:
default_run_args = self.monkeypatch_pip()
else:
default_run_args = self.DEFAULT_RUN_ARGS
VCS_SUPPORT = VcsSupport()
backend = VCS_SUPPORT.get_backend(self.vcs_type)
if backend.run_command.__func__.__defaults__ != default_run_args:
backend.run_command.__func__.__defaults__ = default_run_args
return backend
@property
def is_local(self) -> bool:
url = self.url
if "+" in url:
url = url.split("+")[1]
return url.startswith("file")
def obtain(self, verbosity=1) -> None:
if os.path.exists(
self.checkout_directory
) and not self.repo_backend.is_repository_directory(self.checkout_directory):
self.repo_backend.unpack(self.checkout_directory)
elif not os.path.exists(self.checkout_directory):
self.repo_backend.obtain(self.checkout_directory, self.parsed_url, verbosity)
else:
if self.ref:
self.checkout_ref(self.ref)
if not self.commit_sha:
self.commit_sha = self.commit_hash
def checkout_ref(self, ref: str) -> None:
rev_opts = self.repo_backend.make_rev_options(ref)
if not any(
[
self.repo_backend.is_commit_id_equal(self.checkout_directory, ref),
self.repo_backend.is_commit_id_equal(self.checkout_directory, rev_opts),
self.is_local,
]
):
self.update(ref)
def update(self, ref: str) -> None:
target_ref = self.repo_backend.make_rev_options(ref)
self.repo_backend.update(self.checkout_directory, self.url, target_ref)
self.commit_sha = self.commit_hash
@cached_property
def commit_hash(self) -> str:
return self.repo_backend.get_revision(self.checkout_directory)
@classmethod
def monkeypatch_pip(cls) -> Tuple[Any, ...]:
target_module = VcsSupport.__module__
pip_vcs = importlib.import_module(target_module)
run_command_defaults = pip_vcs.VersionControl.run_command.__func__.__defaults__
new_defaults = [False] + list(run_command_defaults)[1:]
new_defaults = tuple(new_defaults)
pip_vcs.VersionControl.run_command.__func__.__defaults__ = new_defaults
sys.modules[target_module] = pip_vcs
cls.DEFAULT_RUN_ARGS = new_defaults
return new_defaults
-1
View File
@@ -11,7 +11,6 @@ ptyprocess==0.7.0
pydantic==1.10.10
python-dotenv==1.0.0
pythonfinder==2.0.5
requirementslib==3.0.0
ruamel.yaml==0.17.21
shellingham==1.5.0.post1
tomli==2.0.1
+2 -3
View File
@@ -22,8 +22,7 @@ if sys.argv[-1] == "publish":
required = [
"certifi",
"setuptools>=67.0.0",
"virtualenv-clone>=0.2.5",
"virtualenv>=20.17.1",
"virtualenv>=20.24.2",
]
extras = {
"dev": [
@@ -31,7 +30,7 @@ extras = {
"beautifulsoup4",
"sphinx",
"flake8>=3.3.0,<4.0",
"black;python_version>='3.7'",
"black==23.3.0",
"parver",
"invoke",
],
+1 -1
View File
@@ -9,7 +9,7 @@ import invoke
from parver import Version
from pipenv.__version__ import __version__
from pipenv.vendor.requirementslib.utils import temp_environ
from pipenv.utils.shell import temp_environ
from .vendoring import _get_git_root, drop_dir
+1 -2
View File
@@ -15,7 +15,7 @@ import invoke
import requests
from urllib3.util import parse_url as urllib3_parse
from pipenv.vendor.requirementslib.fileutils import open_file
from pipenv.utils.fileutils import open_file
TASK_NAME = "update"
@@ -36,7 +36,6 @@ HARDCODED_LICENSE_URLS = {
"pytoml": "https://github.com/avakar/pytoml/raw/master/LICENSE",
"webencodings": "https://github.com/SimonSapin/python-webencodings/raw/"
"master/LICENSE",
"requirementslib": "https://github.com/techalchemy/requirementslib/raw/master/LICENSE",
"distlib": "https://github.com/vsajip/distlib/raw/master/LICENSE.txt",
"pythonfinder": "https://raw.githubusercontent.com/techalchemy/pythonfinder/master/LICENSE.txt",
"pipdeptree": "https://raw.githubusercontent.com/tox-dev/pipdeptree/main/LICENSE",
+21 -16
View File
@@ -12,11 +12,12 @@ from tempfile import TemporaryDirectory
import subprocess
import pytest
import requests
from pipenv.utils.processes import subprocess_run
from pipenv.patched.pip._vendor import requests
from pipenv.vendor import tomlkit
from pipenv.vendor.requirementslib.utils import temp_environ
from pipenv.vendor.requirementslib.models.setup_info import handle_remove_readonly
from pipenv.utils.processes import subprocess_run
from pipenv.utils.funktools import handle_remove_readonly
from pipenv.utils.shell import temp_environ
log = logging.getLogger(__name__)
warnings.simplefilter("default", category=ResourceWarning)
@@ -173,7 +174,7 @@ class _Pipfile:
@classmethod
def get_fixture_path(cls, path, fixtures="test_artifacts"):
return Path(__file__).absolute().parent.parent / fixtures / path
return Path(__file__).resolve().parent.parent / fixtures / path
class _PipenvInstance:
@@ -284,17 +285,21 @@ class _PipenvInstance:
return os.sep.join([self.path, 'Pipfile.lock'])
# Windows python3.8 fails without this patch. Additional details: https://bugs.python.org/issue42796
def _rmtree_func(path, ignore_errors=True, onerror=None):
shutil_rmtree = _rmtree
if onerror is None:
onerror = handle_remove_readonly
try:
shutil_rmtree(path, ignore_errors=ignore_errors, onerror=onerror)
except (OSError, FileNotFoundError, PermissionError) as exc:
# Ignore removal failures where the file doesn't exist
if exc.errno != errno.ENOENT:
raise
if sys.version_info[:2] <= (3, 8):
# Windows python3.8 fails without this patch. Additional details: https://bugs.python.org/issue42796
def _rmtree_func(path, ignore_errors=True, onerror=None):
shutil_rmtree = _rmtree
if onerror is None:
onerror = handle_remove_readonly
try:
shutil_rmtree(path, ignore_errors=ignore_errors, onerror=onerror)
except (OSError, FileNotFoundError, PermissionError) as exc:
# Ignore removal failures where the file doesn't exist
if exc.errno != errno.ENOENT:
raise
else:
_rmtree_func = _rmtree
@pytest.fixture()
def pipenv_instance_pypi(capfdbinary, monkeypatch):
+5 -2
View File
@@ -1,5 +1,6 @@
import os
import re
import sys
from pathlib import Path
import pytest
@@ -28,6 +29,7 @@ def test_pipenv_venv(pipenv_instance_pypi):
@pytest.mark.cli
@pytest.mark.skipif(sys.version_info[:2] == (3, 8) and os.name == "nt", reason="Python 3.8 on Windows is not supported")
def test_pipenv_py(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv('--python python')
@@ -39,6 +41,7 @@ def test_pipenv_py(pipenv_instance_pypi):
@pytest.mark.cli
@pytest.mark.skipif(os.name == 'nt' and sys.version_info[:2] == (3, 8), reason='Test issue with windows 3.8 CIs')
def test_pipenv_site_packages(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv('--python python --site-packages')
@@ -172,8 +175,8 @@ def test_pipenv_check_check_lockfile_categories(pipenv_instance_pypi, category):
@pytest.mark.cli
def test_pipenv_clean(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
def test_pipenv_clean(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open('setup.py', 'w') as f:
f.write('from setuptools import setup; setup(name="empty")')
c = p.pipenv('install -e .')
+27 -13
View File
@@ -1,9 +1,11 @@
import os
from pathlib import Path
import tempfile
from unittest import mock
import pytest
from pipenv.patched.pip._internal.operations.prepare import File
from pipenv.utils.requirements import import_requirements
from pipenv.project import Project
@@ -11,57 +13,69 @@ from pipenv.project import Project
@pytest.mark.cli
@pytest.mark.deploy
@pytest.mark.system
def test_auth_with_pw_redacted(pipenv_instance_pypi):
@mock.patch("pipenv.utils.dependencies.unpack_url", mock.MagicMock(return_value=File("/some/path/to/project", content_type=None)))
@mock.patch("pipenv.utils.dependencies.find_package_name_from_directory")
def test_auth_with_pw_redacted(mock_find_package_name_from_directory, pipenv_instance_pypi):
mock_find_package_name_from_directory.return_value = "myproject"
with pipenv_instance_pypi() as p:
p.pipenv("run shell")
project = Project()
requirements_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
requirements_file.write("""git+https://${AUTH_USER}:mypw1@github.com/user/myproject.git#egg=myproject""")
requirements_file.write("""git+https://${AUTH_USER}:mypw1@github.com/user/myproject.git@main#egg=myproject""")
requirements_file.close()
import_requirements(project, r=requirements_file.name)
os.unlink(requirements_file.name)
assert p.pipfile["packages"]["myproject"] == {'git': 'https://${AUTH_USER}:****@github.com/user/myproject.git'}
assert p.pipfile["packages"]["myproject"] == {'git': 'git+https://${AUTH_USER}:****@github.com/user/myproject.git', 'ref': 'main'}
@pytest.mark.cli
@pytest.mark.deploy
@pytest.mark.system
def test_auth_with_username_redacted(pipenv_instance_pypi):
@mock.patch("pipenv.utils.dependencies.unpack_url", mock.MagicMock(return_value=File("/some/path/to/project", content_type=None)))
@mock.patch("pipenv.utils.dependencies.find_package_name_from_directory")
def test_auth_with_username_redacted(mock_find_package_name_from_directory, pipenv_instance_pypi):
mock_find_package_name_from_directory.return_value = "myproject"
with pipenv_instance_pypi() as p:
p.pipenv("run shell")
project = Project()
requirements_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
requirements_file.write("""git+https://username@github.com/user/myproject.git#egg=myproject""")
requirements_file.write("""git+https://username@github.com/user/myproject.git@main#egg=myproject""")
requirements_file.close()
import_requirements(project, r=requirements_file.name)
os.unlink(requirements_file.name)
assert p.pipfile["packages"]["myproject"] == {'git': 'https://****@github.com/user/myproject.git'}
assert p.pipfile["packages"]["myproject"] == {'git': 'git+https://****@github.com/user/myproject.git', 'ref': 'main'}
@pytest.mark.cli
@pytest.mark.deploy
@pytest.mark.system
def test_auth_with_pw_are_variables_passed_to_pipfile(pipenv_instance_pypi):
@mock.patch("pipenv.utils.dependencies.unpack_url", mock.MagicMock(return_value=File("/some/path/to/project", content_type=None)))
@mock.patch("pipenv.utils.dependencies.find_package_name_from_directory")
def test_auth_with_pw_are_variables_passed_to_pipfile(mock_find_package_name_from_directory, pipenv_instance_pypi):
mock_find_package_name_from_directory.return_value = "myproject"
with pipenv_instance_pypi() as p:
p.pipenv("run shell")
project = Project()
requirements_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
requirements_file.write("""git+https://${AUTH_USER}:${AUTH_PW}@github.com/user/myproject.git#egg=myproject""")
requirements_file.write("""git+https://${AUTH_USER}:${AUTH_PW}@github.com/user/myproject.git@main#egg=myproject""")
requirements_file.close()
import_requirements(project, r=requirements_file.name)
os.unlink(requirements_file.name)
assert p.pipfile["packages"]["myproject"] == {'git': 'https://${AUTH_USER}:${AUTH_PW}@github.com/user/myproject.git'}
assert p.pipfile["packages"]["myproject"] == {'git': 'git+https://${AUTH_USER}:${AUTH_PW}@github.com/user/myproject.git', 'ref': 'main'}
@pytest.mark.cli
@pytest.mark.deploy
@pytest.mark.system
def test_auth_with_only_username_variable_passed_to_pipfile(pipenv_instance_pypi):
@mock.patch("pipenv.utils.dependencies.unpack_url", mock.MagicMock(return_value=File("/some/path/to/project", content_type=None)))
@mock.patch("pipenv.utils.dependencies.find_package_name_from_directory")
def test_auth_with_only_username_variable_passed_to_pipfile(mock_find_package_name_from_directory, pipenv_instance_pypi):
mock_find_package_name_from_directory.return_value = "myproject"
with pipenv_instance_pypi() as p:
p.pipenv("run shell")
project = Project()
requirements_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
requirements_file.write("""git+https://${AUTH_USER}@github.com/user/myproject.git#egg=myproject""")
requirements_file.write("""git+https://${AUTH_USER}@github.com/user/myproject.git@main#egg=myproject""")
requirements_file.close()
import_requirements(project, r=requirements_file.name)
os.unlink(requirements_file.name)
assert p.pipfile["packages"]["myproject"] == {'git': 'https://${AUTH_USER}@github.com/user/myproject.git'}
assert p.pipfile["packages"]["myproject"] == {'git': 'git+https://${AUTH_USER}@github.com/user/myproject.git', 'ref': 'main'}
+6 -5
View File
@@ -162,6 +162,7 @@ dataclasses-json = "==0.5.7"
@pytest.mark.install
@pytest.mark.resolver
@pytest.mark.backup_resolver
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Package does not work with Python 3.12")
def test_backup_resolver(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
@@ -298,10 +299,10 @@ def test_clean_on_empty_venv(pipenv_instance_pypi):
@pytest.mark.basic
@pytest.mark.install
def test_install_does_not_extrapolate_environ(pipenv_instance_pypi):
def test_install_does_not_extrapolate_environ(pipenv_instance_private_pypi):
"""Ensure environment variables are not expanded in lock file.
"""
with temp_environ(), pipenv_instance_pypi() as p:
with temp_environ(), pipenv_instance_private_pypi() as p:
os.environ["PYPI_URL"] = p.pypi
with open(p.pipfile_path, "w") as f:
@@ -321,7 +322,7 @@ name = 'mockpi'
assert p.lockfile["_meta"]["sources"][0]["url"] == "${PYPI_URL}/simple"
# Ensure package install does not extrapolate.
c = p.pipenv("install six")
c = p.pipenv("install six -v")
assert c.returncode == 0
assert p.pipfile["source"][0]["url"] == "${PYPI_URL}/simple"
assert p.lockfile["_meta"]["sources"][0]["url"] == "${PYPI_URL}/simple"
@@ -528,9 +529,9 @@ def test_install_does_not_exclude_packaging(pipenv_instance_pypi):
@pytest.mark.needs_internet
def test_install_will_supply_extra_pip_args(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv("""install dataclasses-json --extra-pip-args="--use-feature=truststore --proxy=test" """)
c = p.pipenv("""install -v dataclasses-json --extra-pip-args="--use-feature=truststore --proxy=test" """)
assert c.returncode == 1
assert "truststore feature" in c.stderr
assert "truststore feature" in c.stdout
@pytest.mark.basic
+2 -3
View File
@@ -84,7 +84,6 @@ def test_multiple_category_install_proceeds_in_order_specified(pipenv_instance_p
"""Ensure -e .[extras] installs.
"""
with pipenv_instance_private_pypi() as p:
#os.mkdir(os.path.join(p.path, "testpipenv"))
setup_py = os.path.join(p.path, "setup.py")
with open(setup_py, "w") as fh:
contents = """
@@ -106,12 +105,12 @@ setup(
with open(os.path.join(p.path, 'Pipfile'), 'w') as fh:
fh.write("""
[packages]
testpipenv = {path = ".", editable = true}
testpipenv = {path = ".", editable = true, skip_resolver = true}
[prereq]
six = "*"
""".strip())
c = p.pipenv("lock")
c = p.pipenv("lock -v")
assert c.returncode == 0
assert "testpipenv" in p.lockfile["default"]
assert "testpipenv" not in p.lockfile["prereq"]
+5 -1
View File
@@ -28,7 +28,11 @@ fake_package = {}
c = p.pipenv('install -v')
assert c.returncode == 0
assert 'markers' in p.lockfile['default']['fake-package'], p.lockfile["default"]
assert 'markers' in p.lockfile['default']['fake_package'], p.lockfile["default"]
assert p.lockfile['default']['fake_package']['markers'] == "os_name == 'splashwear'"
assert p.lockfile['default']['fake_package']['hashes'] == [
'sha256:1531e01a7f306f496721f425c8404f3cfd8d4933ee6daf4668fcc70059b133f3',
'sha256:cf83dc3f6c34050d3360fbdf655b2652c56532e3028b1c95202611ba1ebdd624']
c = p.pipenv('run python -c "import fake_package;"')
assert c.returncode == 1
+8 -3
View File
@@ -1,11 +1,15 @@
import pytest
from .conftest import DEFAULT_PRIVATE_PYPI_SERVER
@pytest.mark.urls
@pytest.mark.extras
@pytest.mark.install
def test_install_uri_with_extras(pipenv_instance_private_pypi):
file_uri = "http://localhost:8080/packages/plette/plette-0.2.2-py2.py3-none-any.whl"
with pipenv_instance_private_pypi() as p:
def test_install_uri_with_extras(pipenv_instance_pypi):
server = DEFAULT_PRIVATE_PYPI_SERVER.replace("/simple", "")
file_uri = f"{server}/packages/plette/plette-0.2.2-py2.py3-none-any.whl"
with pipenv_instance_pypi() as p:
with open(p.pipfile_path, 'w') as f:
contents = f"""
[[source]]
@@ -20,3 +24,4 @@ plette = {{file = "{file_uri}", extras = ["validation"]}}
c = p.pipenv("install")
assert c.returncode == 0
assert "plette" in p.lockfile["default"]
assert "cerberus" in p.lockfile["default"]
+13 -2
View File
@@ -52,7 +52,7 @@ testpipenv = {path = ".", editable = true, extras = ["dev"]}
c = p.pipenv(f"install {line}")
assert c.returncode == 0
assert "testpipenv" in p.pipfile["packages"]
assert p.pipfile["packages"]["testpipenv"]["path"] == "."
assert p.pipfile["packages"]["testpipenv"]["file"] == "."
assert p.pipfile["packages"]["testpipenv"]["extras"] == ["dev"]
assert "six" in p.lockfile["default"]
@@ -178,7 +178,7 @@ def test_local_package(pipenv_instance_private_pypi, testsroot):
@pytest.mark.files
@pytest.mark.local
def test_local_zip_file(pipenv_instance_private_pypi, testsroot):
def test_local_tar_gz_file(pipenv_instance_private_pypi, testsroot):
file_name = "requests-2.19.1.tar.gz"
with pipenv_instance_private_pypi() as p:
@@ -275,3 +275,14 @@ def test_outdated_should_compare_postreleases_without_failing(pipenv_instance_pr
c = p.pipenv("update --outdated")
assert c.returncode != 0
assert "out-of-date" in c.stdout
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Package does not work with Python 3.12")
def test_install_remote_wheel_file_with_extras(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv("install fastapi[dev]@https://files.pythonhosted.org/packages/4e/1a/04887c641b67e6649bde845b9a631f73a7abfbe3afda83957e09b95d88eb/fastapi-0.95.2-py3-none-any.whl")
assert c.returncode == 0
assert "ruff" in p.lockfile["default"]
assert "pre-commit" in p.lockfile["default"]
assert "uvicorn" in p.lockfile["default"]
+16 -16
View File
@@ -1,4 +1,5 @@
import os
import sys
from pathlib import Path
import pytest
@@ -17,12 +18,13 @@ def test_basic_vcs_install_with_env_var(pipenv_instance_pypi):
# edge case where normal package starts with VCS name shouldn't be flagged as vcs
os.environ["GIT_HOST"] = "github.com"
cli_runner = CliRunner(mix_stderr=False)
c = cli_runner.invoke(cli, "install git+https://${GIT_HOST}/benjaminp/six.git@1.11.0#egg=six gitdb2")
c = cli_runner.invoke(cli, "install -v git+https://${GIT_HOST}/benjaminp/six.git@1.11.0 gitdb2")
assert c.exit_code == 0
assert all(package in p.pipfile["packages"] for package in ["six", "gitdb2"])
assert "git" in p.pipfile["packages"]["six"]
assert p.lockfile["default"]["six"] == {
"git": "https://${GIT_HOST}/benjaminp/six.git",
"git": "git+https://${GIT_HOST}/benjaminp/six.git",
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"ref": "15e31431af97e5e64b80af0a3f598d382bcdd49a",
}
assert "gitdb2" in p.lockfile["default"]
@@ -43,7 +45,6 @@ def test_urls_work(pipenv_instance_pypi):
dep = list(p.pipfile["packages"].values())[0]
assert "file" in dep, p.pipfile
# now that we handle resolution with requirementslib, this will resolve to a name
dep = p.lockfile["default"]["dataclasses-json"]
assert "file" in dep, p.lockfile
@@ -75,10 +76,10 @@ def test_file_urls_work(pipenv_instance_pypi):
@pytest.mark.urls
@pytest.mark.install
@pytest.mark.needs_internet
def test_editable_vcs_install(pipenv_instance_pypi):
def test_vcs_install(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv(
"install -e git+https://github.com/lidatong/dataclasses-json.git#egg=dataclasses-json"
"install git+https://github.com/lidatong/dataclasses-json.git@v0.5.7"
)
assert c.returncode == 0
assert "dataclasses-json" in p.pipfile["packages"]
@@ -88,10 +89,10 @@ def test_editable_vcs_install(pipenv_instance_pypi):
@pytest.mark.urls
@pytest.mark.install
@pytest.mark.needs_internet
def test_install_editable_git_tag(pipenv_instance_private_pypi):
def test_install_git_tag(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
c = p.pipenv(
"install -e git+https://github.com/benjaminp/six.git@1.11.0#egg=six"
"install git+https://github.com/benjaminp/six.git@1.11.0"
)
assert c.returncode == 0
assert "six" in p.pipfile["packages"]
@@ -99,7 +100,7 @@ def test_install_editable_git_tag(pipenv_instance_private_pypi):
assert "git" in p.lockfile["default"]["six"]
assert (
p.lockfile["default"]["six"]["git"]
== "https://github.com/benjaminp/six.git"
== "git+https://github.com/benjaminp/six.git"
)
assert "ref" in p.lockfile["default"]["six"]
@@ -108,6 +109,7 @@ def test_install_editable_git_tag(pipenv_instance_private_pypi):
@pytest.mark.index
@pytest.mark.install
@pytest.mark.needs_internet
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Package does not work with Python 3.12")
def test_install_named_index_alias(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
@@ -136,6 +138,7 @@ six = "*"
@pytest.mark.index
@pytest.mark.install
@pytest.mark.needs_internet
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Package does not work with Python 3.12")
def test_install_specifying_index_url(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
with open(p.pipfile_path, "w") as f:
@@ -184,7 +187,7 @@ def test_install_local_vcs_not_in_lockfile(pipenv_instance_pypi):
def test_get_vcs_refs(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
c = p.pipenv(
"install -e git+https://github.com/benjaminp/six.git@1.9.0#egg=six"
"install -e git+https://github.com/benjaminp/six.git@1.9.0"
)
assert c.returncode == 0
assert "six" in p.pipfile["packages"]
@@ -236,11 +239,8 @@ Jinja2 = {{ref = "2.11.0", git = "{jinja2_uri}"}}
assert all(k in p.pipfile["packages"] for k in installed_packages)
assert all(k.lower() in p.lockfile["default"] for k in installed_packages)
assert all(k in p.lockfile["default"]["jinja2"] for k in ["ref", "git"]), str(p.lockfile["default"])
assert p.lockfile["default"]["jinja2"].get("ref") is not None
assert (
p.lockfile["default"]["jinja2"]["git"]
== jinja2_uri
)
assert p.lockfile["default"]["jinja2"].get("ref") == "bbdafe33ce9f47e3cbfb9415619e354349f11243"
assert p.lockfile["default"]["jinja2"]["git"] == f"{jinja2_uri}"
@pytest.mark.vcs
@@ -249,8 +249,8 @@ Jinja2 = {{ref = "2.11.0", git = "{jinja2_uri}"}}
@pytest.mark.needs_internet
def test_vcs_can_use_markers(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
path = p._pipfile.get_fixture_path("git/six/.git")
p._pipfile.install("six", {"git": f"{path.as_uri()}", "markers": "sys_platform == 'linux'"})
path = p._pipfile.get_fixture_path("git/six/")
p._pipfile.install("six", {"git": f"{path.as_uri()}", "ref": "1.11.0", "markers": "sys_platform == 'linux'"})
assert "six" in p.pipfile["packages"]
c = p.pipenv("install")
assert c.returncode == 0
+9 -12
View File
@@ -157,10 +157,7 @@ def test_resolve_skip_unmatched_requirements(pipenv_instance_pypi):
p._pipfile.add("missing-package", {"markers": "os_name=='FakeOS'"})
c = p.pipenv("lock")
assert c.returncode == 0
assert (
"Could not find a version of missing-package ; "
"os_name == 'FakeOS' that matches your environment"
) in c.stderr
assert 'Could not find a matching version of missing-package; os_name == "FakeOS"' in c.stderr
@pytest.mark.lock
@@ -358,7 +355,7 @@ requests = {git = "%s@883caaf", editable = true}
""".strip() % requests_uri)
c = p.pipenv('lock')
assert c.returncode == 0
assert p.lockfile['default']['requests']['git'] == requests_uri
assert requests_uri in p.lockfile['default']['requests']['git']
assert p.lockfile['default']['requests']['ref'] == '883caaf145fbe93bd0d208a6b864de9146087312'
@@ -509,21 +506,21 @@ def test_lock_nested_vcs_direct_url(pipenv_instance_pypi):
assert "git" in p.lockfile["default"]["pep508-package"]
assert "sibling-package" in p.lockfile["default"]
assert "git" in p.lockfile["default"]["sibling-package"]
assert "subdirectory" in p.lockfile["default"]["sibling-package"]
assert "subdirectory" in p.lockfile["default"]["sibling-package"]["git"]
assert "version" not in p.lockfile["default"]["sibling-package"]
@pytest.mark.lock
@pytest.mark.install
def test_lock_package_with_wildcard_version(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv("install 'six==1.11.*'")
def test_lock_package_with_compatible_release_specifier(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
c = p.pipenv("install six~=1.11")
assert c.returncode == 0
assert "six" in p.pipfile["packages"]
assert p.pipfile["packages"]["six"] == "==1.11.*"
assert p.pipfile["packages"]["six"] == "~=1.11"
assert "six" in p.lockfile["default"]
assert "version" in p.lockfile["default"]["six"]
assert p.lockfile["default"]["six"]["version"] == "==1.11.0"
assert p.lockfile["default"]["six"]["version"] == "==1.12.0"
@pytest.mark.lock
@@ -649,4 +646,4 @@ dataclasses-json = {extras = ["dev"], version = "==0.5.7"}
assert c.returncode == 0
assert "dataclasses-json" in p.pipfile["packages"]
assert "dataclasses-json" in p.lockfile["default"]
assert "markers" not in p.lockfile["default"]["dataclasses-json"]
assert p.lockfile["default"]["dataclasses-json"].get("markers", "") is not None
+1 -1
View File
@@ -6,7 +6,7 @@ import pytest
from pipenv.project import Project
from pipenv.utils.shell import temp_environ
from pipenv.vendor.plette import Pipfile
from pipenv.vendor.requirementslib.fileutils import normalize_path
from pipenv.utils.fileutils import normalize_path
@pytest.mark.project
+4 -3
View File
@@ -3,7 +3,8 @@ import os
import pytest
from pipenv.utils.shell import temp_environ
from pipenv.routines.requirements import requirements_from_deps
from pipenv.utils.requirements import requirements_from_lockfile
@pytest.mark.requirements
def test_requirements_generates_requirements_from_lockfile(pipenv_instance_pypi):
@@ -289,11 +290,11 @@ def test_requirements_generates_requirements_from_lockfile_without_env_var_expan
},
True,
True,
["pyjwt[crypto] @ git+https://github.com/jpadilla/pyjwt.git@7665aa625506a11bae50b56d3e04413a3dc6fdf8"]
["pyjwt[crypto]@ git+https://github.com/jpadilla/pyjwt.git@7665aa625506a11bae50b56d3e04413a3dc6fdf8"]
)
]
)
def test_requirements_from_deps(deps, include_hashes, include_markers, expected):
result = requirements_from_deps(deps, include_hashes, include_markers)
result = requirements_from_lockfile(deps, include_hashes, include_markers)
assert result == expected
-1
View File
@@ -37,7 +37,6 @@ multicommand = "bash -c \"cd docs && make html\""
c = p.pipenv('run printfoo')
assert c.returncode == 0
assert c.stdout.strip() == 'foo'
assert not c.stderr.strip()
c = p.pipenv('run notfoundscript')
assert c.returncode != 0
+7 -15
View File
@@ -1,4 +1,5 @@
import pytest
import sys
from .conftest import DEFAULT_PRIVATE_PYPI_SERVER
@@ -7,28 +8,20 @@ from pipenv.utils.shell import temp_environ
@pytest.mark.uninstall
@pytest.mark.install
def test_uninstall_requests(pipenv_instance_private_pypi):
# Uninstalling requests can fail even when uninstall Django below
# succeeds, if requests was de-vendored.
# See https://github.com/pypa/pipenv/issues/3644 for problems
# caused by devendoring
with pipenv_instance_private_pypi() as p:
def test_uninstall_requests(pipenv_instance_pypi):
with pipenv_instance_pypi() as p:
c = p.pipenv("install requests")
assert c.returncode == 0
assert "requests" in p.pipfile["packages"]
c = p.pipenv("run python -m requests.help")
assert c.returncode == 0
c = p.pipenv("uninstall requests")
assert c.returncode == 0
assert "requests" not in p.pipfile["dev-packages"]
c = p.pipenv("run python -m requests.help")
assert c.returncode > 0
assert "requests" not in p.pipfile["packages"]
assert "requests" not in p.lockfile["default"]
@pytest.mark.uninstall
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Package does not work with Python 3.12")
def test_uninstall_django(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
c = p.pipenv("install Django")
@@ -52,6 +45,7 @@ def test_uninstall_django(pipenv_instance_private_pypi):
@pytest.mark.install
@pytest.mark.uninstall
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Package does not work with Python 3.12")
def test_mirror_uninstall(pipenv_instance_pypi):
with temp_environ(), pipenv_instance_pypi() as p:
@@ -132,8 +126,6 @@ six = "==1.12.0"
assert "tablib" in p.lockfile["default"]
assert "jinja2" in p.lockfile["develop"]
assert "six" in p.lockfile["develop"]
c = p.pipenv('run python -c "import jinja2"')
assert c.returncode == 0
c = p.pipenv("uninstall --all-dev")
+2 -1
View File
@@ -1,5 +1,6 @@
import pytest
@pytest.mark.parametrize("cmd_option", ["", "--dev"])
@pytest.mark.basic
@pytest.mark.update
@@ -7,5 +8,5 @@ def test_update_outdated_with_outdated_package(pipenv_instance_private_pypi, cmd
with pipenv_instance_private_pypi() as p:
package_name = "six"
p.pipenv(f"install {cmd_option} {package_name}==1.11")
c = p.pipenv("update --outdated")
c = p.pipenv(f"update {package_name} {cmd_option} --outdated")
assert f"Package '{package_name}' out-of-date:" in c.stdout
+2 -2
View File
@@ -48,7 +48,7 @@ def test_local_path_windows(pipenv_instance_pypi):
except OSError:
whl = whl.absolute()
with pipenv_instance_pypi() as p:
c = p.pipenv(f'install "{whl}"')
c = p.pipenv(f'install "{whl}" -v')
assert c.returncode == 0
@@ -64,7 +64,7 @@ def test_local_path_windows_forward_slash(pipenv_instance_pypi):
except OSError:
whl = whl.absolute()
with pipenv_instance_pypi() as p:
c = p.pipenv(f'install "{whl.as_posix()}"')
c = p.pipenv(f'install "{whl.as_posix()}" -v')
assert c.returncode == 0
+11 -14
View File
@@ -25,11 +25,11 @@ DEP_PIP_PAIRS = [
"editable": True,
}
},
"-e git+https://github.com/lidatong/dataclasses-json.git@v0.5.7#egg=dataclasses-json",
"dataclasses-json@ git+https://github.com/lidatong/dataclasses-json.git@v0.5.7",
),
(
{"dataclasses-json": {"git": "https://github.com/lidatong/dataclasses-json.git", "ref": "v0.5.7"}},
"git+https://github.com/lidatong/dataclasses-json.git@v0.5.7#egg=dataclasses-json",
"dataclasses-json@ git+https://github.com/lidatong/dataclasses-json.git@v0.5.7",
),
(
# Extras in url
@@ -39,7 +39,7 @@ DEP_PIP_PAIRS = [
"extras": ["pipenv"],
}
},
"https://github.com/oz123/dparse/archive/refs/heads/master.zip#egg=dparse[pipenv]"
"dparse[pipenv] @ https://github.com/oz123/dparse/archive/refs/heads/master.zip",
),
(
{
@@ -50,7 +50,7 @@ DEP_PIP_PAIRS = [
"editable": False,
}
},
"git+https://github.com/requests/requests.git@main#egg=requests[security]",
"requests[security]@ git+https://github.com/requests/requests.git@main",
),
]
@@ -64,8 +64,6 @@ def mock_unpack(link, source_dir, download_dir, only_download=False, session=Non
@pytest.mark.parametrize("deps, expected", DEP_PIP_PAIRS)
@pytest.mark.needs_internet
def test_convert_deps_to_pip(deps, expected):
if expected.startswith("Django"):
expected = expected.lower()
assert dependencies.convert_deps_to_pip(deps) == [expected]
@@ -136,13 +134,12 @@ def test_convert_deps_to_pip_one_way():
@pytest.mark.parametrize(
"deps, expected",
[
({"uvicorn": {}}, ["uvicorn"]),
({"FooProject": {"path": ".", "editable": "true"}}, []),
({"FooProject": {"version": "==1.2"}}, ["fooproject==1.2"]),
({"uvicorn": {"extras": ["standard"]}}, []),
({"uvicorn": {"extras": []}}, ["uvicorn"]),
({"extras": {}}, ["extras"]),
({"uvicorn[standard]": {}}, [])
({"uvicorn": {}}, {"uvicorn"}),
({"FooProject": {"path": ".", "editable": "true"}}, set()),
({"FooProject": {"version": "==1.2"}}, {"fooproject==1.2"}),
({"uvicorn": {"extras": ["standard"]}}, {"uvicorn"}),
({"uvicorn": {"extras": []}}, {"uvicorn"}),
({"extras": {}}, {"extras"}),
],
)
def test_get_constraints_from_deps(deps, expected):
@@ -209,7 +206,7 @@ class TestUtils:
)
@pytest.mark.vcs
def test_is_vcs(self, entry, expected):
from pipenv.vendor.requirementslib.utils import is_vcs
from pipenv.utils.requirementslib import is_vcs
assert is_vcs(entry) is expected
@pytest.mark.utils
+1 -7
View File
@@ -3,7 +3,7 @@
import pipenv # noqa
import datetime
import os
import pytest
import pytz
@@ -43,9 +43,3 @@ from pipenv.vendor import tomlkit
def test_token_date(dt, content):
item = tomlkit.item(dt)
assert item.as_string() == content
def test_dump_nonascii_string():
content = 'name = "Stažené"\n'
toml_content = tomlkit.dumps(tomlkit.loads(content))
assert toml_content == content