Text Generation
Transformers
Safetensors
PEFT
English
nemotron_h
telecommunications
3gpp
o-ran
ietf
telecom
lora
nemotron
mixture-of-experts
gsma
network-slicing
anomaly-detection
srsran
conversational
custom_code
Eval Results (legacy)
Instructions to use tharaka-axonect/nemotron-testing with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use tharaka-axonect/nemotron-testing with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="tharaka-axonect/nemotron-testing", trust_remote_code=True) messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("tharaka-axonect/nemotron-testing", trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained("tharaka-axonect/nemotron-testing", trust_remote_code=True, device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - PEFT
How to use tharaka-axonect/nemotron-testing with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use tharaka-axonect/nemotron-testing with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "tharaka-axonect/nemotron-testing" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tharaka-axonect/nemotron-testing", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/tharaka-axonect/nemotron-testing
- SGLang
How to use tharaka-axonect/nemotron-testing with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "tharaka-axonect/nemotron-testing" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tharaka-axonect/nemotron-testing", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "tharaka-axonect/nemotron-testing" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "tharaka-axonect/nemotron-testing", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use tharaka-axonect/nemotron-testing with Docker Model Runner:
docker model run hf.co/tharaka-axonect/nemotron-testing
Upload folder using huggingface_hub (part 5)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py +46 -0
- .env/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py +738 -0
- .env/lib/python3.13/site-packages/pip/_internal/operations/prepare.py +737 -0
- .env/lib/python3.13/site-packages/pip/_internal/pyproject.py +185 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__init__.py +103 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/__init__.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/constructors.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_dependency_group.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_file.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_install.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_set.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_uninstall.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/constructors.py +560 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py +79 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/req_file.py +623 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/req_install.py +934 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/req_set.py +82 -0
- .env/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py +636 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/__init__.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/base.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/base.py +20 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/__init__.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/resolver.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py +597 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/__init__.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/base.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/candidates.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/found_candidates.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/provider.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/reporter.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/resolver.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py +139 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py +579 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py +823 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py +164 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py +281 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py +83 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py +245 -0
- .env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py +320 -0
- .env/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py +252 -0
- .env/lib/python3.13/site-packages/pip/_internal/utils/__init__.py +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_log.cpython-313.pyc +0 -0
- .env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-313.pyc +0 -0
.env/lib/python3.13/site-packages/pip/_internal/operations/install/editable_legacy.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Legacy editable installation process, i.e. `setup.py develop`."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Optional, Sequence
|
| 5 |
+
|
| 6 |
+
from pip._internal.build_env import BuildEnvironment
|
| 7 |
+
from pip._internal.utils.logging import indent_log
|
| 8 |
+
from pip._internal.utils.setuptools_build import make_setuptools_develop_args
|
| 9 |
+
from pip._internal.utils.subprocess import call_subprocess
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def install_editable(
|
| 15 |
+
*,
|
| 16 |
+
global_options: Sequence[str],
|
| 17 |
+
prefix: Optional[str],
|
| 18 |
+
home: Optional[str],
|
| 19 |
+
use_user_site: bool,
|
| 20 |
+
name: str,
|
| 21 |
+
setup_py_path: str,
|
| 22 |
+
isolated: bool,
|
| 23 |
+
build_env: BuildEnvironment,
|
| 24 |
+
unpacked_source_directory: str,
|
| 25 |
+
) -> None:
|
| 26 |
+
"""Install a package in editable mode. Most arguments are pass-through
|
| 27 |
+
to setuptools.
|
| 28 |
+
"""
|
| 29 |
+
logger.info("Running setup.py develop for %s", name)
|
| 30 |
+
|
| 31 |
+
args = make_setuptools_develop_args(
|
| 32 |
+
setup_py_path,
|
| 33 |
+
global_options=global_options,
|
| 34 |
+
no_user_config=isolated,
|
| 35 |
+
prefix=prefix,
|
| 36 |
+
home=home,
|
| 37 |
+
use_user_site=use_user_site,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
with indent_log():
|
| 41 |
+
with build_env:
|
| 42 |
+
call_subprocess(
|
| 43 |
+
args,
|
| 44 |
+
command_desc="python setup.py develop",
|
| 45 |
+
cwd=unpacked_source_directory,
|
| 46 |
+
)
|
.env/lib/python3.13/site-packages/pip/_internal/operations/install/wheel.py
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Support for installing and building the "wheel" binary package format."""
|
| 2 |
+
|
| 3 |
+
import collections
|
| 4 |
+
import compileall
|
| 5 |
+
import contextlib
|
| 6 |
+
import csv
|
| 7 |
+
import importlib
|
| 8 |
+
import logging
|
| 9 |
+
import os.path
|
| 10 |
+
import re
|
| 11 |
+
import shutil
|
| 12 |
+
import sys
|
| 13 |
+
import warnings
|
| 14 |
+
from base64 import urlsafe_b64encode
|
| 15 |
+
from email.message import Message
|
| 16 |
+
from itertools import chain, filterfalse, starmap
|
| 17 |
+
from typing import (
|
| 18 |
+
IO,
|
| 19 |
+
Any,
|
| 20 |
+
BinaryIO,
|
| 21 |
+
Callable,
|
| 22 |
+
Dict,
|
| 23 |
+
Generator,
|
| 24 |
+
Iterable,
|
| 25 |
+
Iterator,
|
| 26 |
+
List,
|
| 27 |
+
NewType,
|
| 28 |
+
Optional,
|
| 29 |
+
Protocol,
|
| 30 |
+
Sequence,
|
| 31 |
+
Set,
|
| 32 |
+
Tuple,
|
| 33 |
+
Union,
|
| 34 |
+
cast,
|
| 35 |
+
)
|
| 36 |
+
from zipfile import ZipFile, ZipInfo
|
| 37 |
+
|
| 38 |
+
from pip._vendor.distlib.scripts import ScriptMaker
|
| 39 |
+
from pip._vendor.distlib.util import get_export_entry
|
| 40 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 41 |
+
|
| 42 |
+
from pip._internal.exceptions import InstallationError
|
| 43 |
+
from pip._internal.locations import get_major_minor_version
|
| 44 |
+
from pip._internal.metadata import (
|
| 45 |
+
BaseDistribution,
|
| 46 |
+
FilesystemWheel,
|
| 47 |
+
get_wheel_distribution,
|
| 48 |
+
)
|
| 49 |
+
from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl
|
| 50 |
+
from pip._internal.models.scheme import SCHEME_KEYS, Scheme
|
| 51 |
+
from pip._internal.utils.filesystem import adjacent_tmp_file, replace
|
| 52 |
+
from pip._internal.utils.misc import StreamWrapper, ensure_dir, hash_file, partition
|
| 53 |
+
from pip._internal.utils.unpacking import (
|
| 54 |
+
current_umask,
|
| 55 |
+
is_within_directory,
|
| 56 |
+
set_extracted_file_to_default_mode_plus_executable,
|
| 57 |
+
zip_item_is_executable,
|
| 58 |
+
)
|
| 59 |
+
from pip._internal.utils.wheel import parse_wheel
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class File(Protocol):
|
| 63 |
+
src_record_path: "RecordPath"
|
| 64 |
+
dest_path: str
|
| 65 |
+
changed: bool
|
| 66 |
+
|
| 67 |
+
def save(self) -> None:
|
| 68 |
+
pass
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
logger = logging.getLogger(__name__)
|
| 72 |
+
|
| 73 |
+
RecordPath = NewType("RecordPath", str)
|
| 74 |
+
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def rehash(path: str, blocksize: int = 1 << 20) -> Tuple[str, str]:
|
| 78 |
+
"""Return (encoded_digest, length) for path using hashlib.sha256()"""
|
| 79 |
+
h, length = hash_file(path, blocksize)
|
| 80 |
+
digest = "sha256=" + urlsafe_b64encode(h.digest()).decode("latin1").rstrip("=")
|
| 81 |
+
return (digest, str(length))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def csv_io_kwargs(mode: str) -> Dict[str, Any]:
|
| 85 |
+
"""Return keyword arguments to properly open a CSV file
|
| 86 |
+
in the given mode.
|
| 87 |
+
"""
|
| 88 |
+
return {"mode": mode, "newline": "", "encoding": "utf-8"}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def fix_script(path: str) -> bool:
|
| 92 |
+
"""Replace #!python with #!/path/to/python
|
| 93 |
+
Return True if file was changed.
|
| 94 |
+
"""
|
| 95 |
+
# XXX RECORD hashes will need to be updated
|
| 96 |
+
assert os.path.isfile(path)
|
| 97 |
+
|
| 98 |
+
with open(path, "rb") as script:
|
| 99 |
+
firstline = script.readline()
|
| 100 |
+
if not firstline.startswith(b"#!python"):
|
| 101 |
+
return False
|
| 102 |
+
exename = sys.executable.encode(sys.getfilesystemencoding())
|
| 103 |
+
firstline = b"#!" + exename + os.linesep.encode("ascii")
|
| 104 |
+
rest = script.read()
|
| 105 |
+
with open(path, "wb") as script:
|
| 106 |
+
script.write(firstline)
|
| 107 |
+
script.write(rest)
|
| 108 |
+
return True
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def wheel_root_is_purelib(metadata: Message) -> bool:
|
| 112 |
+
return metadata.get("Root-Is-Purelib", "").lower() == "true"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_entrypoints(dist: BaseDistribution) -> Tuple[Dict[str, str], Dict[str, str]]:
|
| 116 |
+
console_scripts = {}
|
| 117 |
+
gui_scripts = {}
|
| 118 |
+
for entry_point in dist.iter_entry_points():
|
| 119 |
+
if entry_point.group == "console_scripts":
|
| 120 |
+
console_scripts[entry_point.name] = entry_point.value
|
| 121 |
+
elif entry_point.group == "gui_scripts":
|
| 122 |
+
gui_scripts[entry_point.name] = entry_point.value
|
| 123 |
+
return console_scripts, gui_scripts
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def message_about_scripts_not_on_PATH(scripts: Sequence[str]) -> Optional[str]:
|
| 127 |
+
"""Determine if any scripts are not on PATH and format a warning.
|
| 128 |
+
Returns a warning message if one or more scripts are not on PATH,
|
| 129 |
+
otherwise None.
|
| 130 |
+
"""
|
| 131 |
+
if not scripts:
|
| 132 |
+
return None
|
| 133 |
+
|
| 134 |
+
# Group scripts by the path they were installed in
|
| 135 |
+
grouped_by_dir: Dict[str, Set[str]] = collections.defaultdict(set)
|
| 136 |
+
for destfile in scripts:
|
| 137 |
+
parent_dir = os.path.dirname(destfile)
|
| 138 |
+
script_name = os.path.basename(destfile)
|
| 139 |
+
grouped_by_dir[parent_dir].add(script_name)
|
| 140 |
+
|
| 141 |
+
# We don't want to warn for directories that are on PATH.
|
| 142 |
+
not_warn_dirs = [
|
| 143 |
+
os.path.normcase(os.path.normpath(i)).rstrip(os.sep)
|
| 144 |
+
for i in os.environ.get("PATH", "").split(os.pathsep)
|
| 145 |
+
]
|
| 146 |
+
# If an executable sits with sys.executable, we don't warn for it.
|
| 147 |
+
# This covers the case of venv invocations without activating the venv.
|
| 148 |
+
not_warn_dirs.append(
|
| 149 |
+
os.path.normcase(os.path.normpath(os.path.dirname(sys.executable)))
|
| 150 |
+
)
|
| 151 |
+
warn_for: Dict[str, Set[str]] = {
|
| 152 |
+
parent_dir: scripts
|
| 153 |
+
for parent_dir, scripts in grouped_by_dir.items()
|
| 154 |
+
if os.path.normcase(os.path.normpath(parent_dir)) not in not_warn_dirs
|
| 155 |
+
}
|
| 156 |
+
if not warn_for:
|
| 157 |
+
return None
|
| 158 |
+
|
| 159 |
+
# Format a message
|
| 160 |
+
msg_lines = []
|
| 161 |
+
for parent_dir, dir_scripts in warn_for.items():
|
| 162 |
+
sorted_scripts: List[str] = sorted(dir_scripts)
|
| 163 |
+
if len(sorted_scripts) == 1:
|
| 164 |
+
start_text = f"script {sorted_scripts[0]} is"
|
| 165 |
+
else:
|
| 166 |
+
start_text = "scripts {} are".format(
|
| 167 |
+
", ".join(sorted_scripts[:-1]) + " and " + sorted_scripts[-1]
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
msg_lines.append(
|
| 171 |
+
f"The {start_text} installed in '{parent_dir}' which is not on PATH."
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
last_line_fmt = (
|
| 175 |
+
"Consider adding {} to PATH or, if you prefer "
|
| 176 |
+
"to suppress this warning, use --no-warn-script-location."
|
| 177 |
+
)
|
| 178 |
+
if len(msg_lines) == 1:
|
| 179 |
+
msg_lines.append(last_line_fmt.format("this directory"))
|
| 180 |
+
else:
|
| 181 |
+
msg_lines.append(last_line_fmt.format("these directories"))
|
| 182 |
+
|
| 183 |
+
# Add a note if any directory starts with ~
|
| 184 |
+
warn_for_tilde = any(
|
| 185 |
+
i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i
|
| 186 |
+
)
|
| 187 |
+
if warn_for_tilde:
|
| 188 |
+
tilde_warning_msg = (
|
| 189 |
+
"NOTE: The current PATH contains path(s) starting with `~`, "
|
| 190 |
+
"which may not be expanded by all applications."
|
| 191 |
+
)
|
| 192 |
+
msg_lines.append(tilde_warning_msg)
|
| 193 |
+
|
| 194 |
+
# Returns the formatted multiline message
|
| 195 |
+
return "\n".join(msg_lines)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def _normalized_outrows(
|
| 199 |
+
outrows: Iterable[InstalledCSVRow],
|
| 200 |
+
) -> List[Tuple[str, str, str]]:
|
| 201 |
+
"""Normalize the given rows of a RECORD file.
|
| 202 |
+
|
| 203 |
+
Items in each row are converted into str. Rows are then sorted to make
|
| 204 |
+
the value more predictable for tests.
|
| 205 |
+
|
| 206 |
+
Each row is a 3-tuple (path, hash, size) and corresponds to a record of
|
| 207 |
+
a RECORD file (see PEP 376 and PEP 427 for details). For the rows
|
| 208 |
+
passed to this function, the size can be an integer as an int or string,
|
| 209 |
+
or the empty string.
|
| 210 |
+
"""
|
| 211 |
+
# Normally, there should only be one row per path, in which case the
|
| 212 |
+
# second and third elements don't come into play when sorting.
|
| 213 |
+
# However, in cases in the wild where a path might happen to occur twice,
|
| 214 |
+
# we don't want the sort operation to trigger an error (but still want
|
| 215 |
+
# determinism). Since the third element can be an int or string, we
|
| 216 |
+
# coerce each element to a string to avoid a TypeError in this case.
|
| 217 |
+
# For additional background, see--
|
| 218 |
+
# https://github.com/pypa/pip/issues/5868
|
| 219 |
+
return sorted(
|
| 220 |
+
(record_path, hash_, str(size)) for record_path, hash_, size in outrows
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _record_to_fs_path(record_path: RecordPath, lib_dir: str) -> str:
|
| 225 |
+
return os.path.join(lib_dir, record_path)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _fs_to_record_path(path: str, lib_dir: str) -> RecordPath:
|
| 229 |
+
# On Windows, do not handle relative paths if they belong to different
|
| 230 |
+
# logical disks
|
| 231 |
+
if os.path.splitdrive(path)[0].lower() == os.path.splitdrive(lib_dir)[0].lower():
|
| 232 |
+
path = os.path.relpath(path, lib_dir)
|
| 233 |
+
|
| 234 |
+
path = path.replace(os.path.sep, "/")
|
| 235 |
+
return cast("RecordPath", path)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def get_csv_rows_for_installed(
|
| 239 |
+
old_csv_rows: List[List[str]],
|
| 240 |
+
installed: Dict[RecordPath, RecordPath],
|
| 241 |
+
changed: Set[RecordPath],
|
| 242 |
+
generated: List[str],
|
| 243 |
+
lib_dir: str,
|
| 244 |
+
) -> List[InstalledCSVRow]:
|
| 245 |
+
"""
|
| 246 |
+
:param installed: A map from archive RECORD path to installation RECORD
|
| 247 |
+
path.
|
| 248 |
+
"""
|
| 249 |
+
installed_rows: List[InstalledCSVRow] = []
|
| 250 |
+
for row in old_csv_rows:
|
| 251 |
+
if len(row) > 3:
|
| 252 |
+
logger.warning("RECORD line has more than three elements: %s", row)
|
| 253 |
+
old_record_path = cast("RecordPath", row[0])
|
| 254 |
+
new_record_path = installed.pop(old_record_path, old_record_path)
|
| 255 |
+
if new_record_path in changed:
|
| 256 |
+
digest, length = rehash(_record_to_fs_path(new_record_path, lib_dir))
|
| 257 |
+
else:
|
| 258 |
+
digest = row[1] if len(row) > 1 else ""
|
| 259 |
+
length = row[2] if len(row) > 2 else ""
|
| 260 |
+
installed_rows.append((new_record_path, digest, length))
|
| 261 |
+
for f in generated:
|
| 262 |
+
path = _fs_to_record_path(f, lib_dir)
|
| 263 |
+
digest, length = rehash(f)
|
| 264 |
+
installed_rows.append((path, digest, length))
|
| 265 |
+
return installed_rows + [
|
| 266 |
+
(installed_record_path, "", "") for installed_record_path in installed.values()
|
| 267 |
+
]
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def get_console_script_specs(console: Dict[str, str]) -> List[str]:
|
| 271 |
+
"""
|
| 272 |
+
Given the mapping from entrypoint name to callable, return the relevant
|
| 273 |
+
console script specs.
|
| 274 |
+
"""
|
| 275 |
+
# Don't mutate caller's version
|
| 276 |
+
console = console.copy()
|
| 277 |
+
|
| 278 |
+
scripts_to_generate = []
|
| 279 |
+
|
| 280 |
+
# Special case pip and setuptools to generate versioned wrappers
|
| 281 |
+
#
|
| 282 |
+
# The issue is that some projects (specifically, pip and setuptools) use
|
| 283 |
+
# code in setup.py to create "versioned" entry points - pip2.7 on Python
|
| 284 |
+
# 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into
|
| 285 |
+
# the wheel metadata at build time, and so if the wheel is installed with
|
| 286 |
+
# a *different* version of Python the entry points will be wrong. The
|
| 287 |
+
# correct fix for this is to enhance the metadata to be able to describe
|
| 288 |
+
# such versioned entry points.
|
| 289 |
+
# Currently, projects using versioned entry points will either have
|
| 290 |
+
# incorrect versioned entry points, or they will not be able to distribute
|
| 291 |
+
# "universal" wheels (i.e., they will need a wheel per Python version).
|
| 292 |
+
#
|
| 293 |
+
# Because setuptools and pip are bundled with _ensurepip and virtualenv,
|
| 294 |
+
# we need to use universal wheels. As a workaround, we
|
| 295 |
+
# override the versioned entry points in the wheel and generate the
|
| 296 |
+
# correct ones.
|
| 297 |
+
#
|
| 298 |
+
# To add the level of hack in this section of code, in order to support
|
| 299 |
+
# ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment
|
| 300 |
+
# variable which will control which version scripts get installed.
|
| 301 |
+
#
|
| 302 |
+
# ENSUREPIP_OPTIONS=altinstall
|
| 303 |
+
# - Only pipX.Y and easy_install-X.Y will be generated and installed
|
| 304 |
+
# ENSUREPIP_OPTIONS=install
|
| 305 |
+
# - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note
|
| 306 |
+
# that this option is technically if ENSUREPIP_OPTIONS is set and is
|
| 307 |
+
# not altinstall
|
| 308 |
+
# DEFAULT
|
| 309 |
+
# - The default behavior is to install pip, pipX, pipX.Y, easy_install
|
| 310 |
+
# and easy_install-X.Y.
|
| 311 |
+
pip_script = console.pop("pip", None)
|
| 312 |
+
if pip_script:
|
| 313 |
+
if "ENSUREPIP_OPTIONS" not in os.environ:
|
| 314 |
+
scripts_to_generate.append("pip = " + pip_script)
|
| 315 |
+
|
| 316 |
+
if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
|
| 317 |
+
scripts_to_generate.append(f"pip{sys.version_info[0]} = {pip_script}")
|
| 318 |
+
|
| 319 |
+
scripts_to_generate.append(f"pip{get_major_minor_version()} = {pip_script}")
|
| 320 |
+
# Delete any other versioned pip entry points
|
| 321 |
+
pip_ep = [k for k in console if re.match(r"pip(\d+(\.\d+)?)?$", k)]
|
| 322 |
+
for k in pip_ep:
|
| 323 |
+
del console[k]
|
| 324 |
+
easy_install_script = console.pop("easy_install", None)
|
| 325 |
+
if easy_install_script:
|
| 326 |
+
if "ENSUREPIP_OPTIONS" not in os.environ:
|
| 327 |
+
scripts_to_generate.append("easy_install = " + easy_install_script)
|
| 328 |
+
|
| 329 |
+
scripts_to_generate.append(
|
| 330 |
+
f"easy_install-{get_major_minor_version()} = {easy_install_script}"
|
| 331 |
+
)
|
| 332 |
+
# Delete any other versioned easy_install entry points
|
| 333 |
+
easy_install_ep = [
|
| 334 |
+
k for k in console if re.match(r"easy_install(-\d+\.\d+)?$", k)
|
| 335 |
+
]
|
| 336 |
+
for k in easy_install_ep:
|
| 337 |
+
del console[k]
|
| 338 |
+
|
| 339 |
+
# Generate the console entry points specified in the wheel
|
| 340 |
+
scripts_to_generate.extend(starmap("{} = {}".format, console.items()))
|
| 341 |
+
|
| 342 |
+
return scripts_to_generate
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
class ZipBackedFile:
|
| 346 |
+
def __init__(
|
| 347 |
+
self, src_record_path: RecordPath, dest_path: str, zip_file: ZipFile
|
| 348 |
+
) -> None:
|
| 349 |
+
self.src_record_path = src_record_path
|
| 350 |
+
self.dest_path = dest_path
|
| 351 |
+
self._zip_file = zip_file
|
| 352 |
+
self.changed = False
|
| 353 |
+
|
| 354 |
+
def _getinfo(self) -> ZipInfo:
|
| 355 |
+
return self._zip_file.getinfo(self.src_record_path)
|
| 356 |
+
|
| 357 |
+
def save(self) -> None:
|
| 358 |
+
# When we open the output file below, any existing file is truncated
|
| 359 |
+
# before we start writing the new contents. This is fine in most
|
| 360 |
+
# cases, but can cause a segfault if pip has loaded a shared
|
| 361 |
+
# object (e.g. from pyopenssl through its vendored urllib3)
|
| 362 |
+
# Since the shared object is mmap'd an attempt to call a
|
| 363 |
+
# symbol in it will then cause a segfault. Unlinking the file
|
| 364 |
+
# allows writing of new contents while allowing the process to
|
| 365 |
+
# continue to use the old copy.
|
| 366 |
+
if os.path.exists(self.dest_path):
|
| 367 |
+
os.unlink(self.dest_path)
|
| 368 |
+
|
| 369 |
+
zipinfo = self._getinfo()
|
| 370 |
+
|
| 371 |
+
# optimization: the file is created by open(),
|
| 372 |
+
# skip the decompression when there is 0 bytes to decompress.
|
| 373 |
+
with open(self.dest_path, "wb") as dest:
|
| 374 |
+
if zipinfo.file_size > 0:
|
| 375 |
+
with self._zip_file.open(zipinfo) as f:
|
| 376 |
+
blocksize = min(zipinfo.file_size, 1024 * 1024)
|
| 377 |
+
shutil.copyfileobj(f, dest, blocksize)
|
| 378 |
+
|
| 379 |
+
if zip_item_is_executable(zipinfo):
|
| 380 |
+
set_extracted_file_to_default_mode_plus_executable(self.dest_path)
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
class ScriptFile:
|
| 384 |
+
def __init__(self, file: "File") -> None:
|
| 385 |
+
self._file = file
|
| 386 |
+
self.src_record_path = self._file.src_record_path
|
| 387 |
+
self.dest_path = self._file.dest_path
|
| 388 |
+
self.changed = False
|
| 389 |
+
|
| 390 |
+
def save(self) -> None:
|
| 391 |
+
self._file.save()
|
| 392 |
+
self.changed = fix_script(self.dest_path)
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
class MissingCallableSuffix(InstallationError):
|
| 396 |
+
def __init__(self, entry_point: str) -> None:
|
| 397 |
+
super().__init__(
|
| 398 |
+
f"Invalid script entry point: {entry_point} - A callable "
|
| 399 |
+
"suffix is required. Cf https://packaging.python.org/"
|
| 400 |
+
"specifications/entry-points/#use-for-scripts for more "
|
| 401 |
+
"information."
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def _raise_for_invalid_entrypoint(specification: str) -> None:
|
| 406 |
+
entry = get_export_entry(specification)
|
| 407 |
+
if entry is not None and entry.suffix is None:
|
| 408 |
+
raise MissingCallableSuffix(str(entry))
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class PipScriptMaker(ScriptMaker):
|
| 412 |
+
def make(
|
| 413 |
+
self, specification: str, options: Optional[Dict[str, Any]] = None
|
| 414 |
+
) -> List[str]:
|
| 415 |
+
_raise_for_invalid_entrypoint(specification)
|
| 416 |
+
return super().make(specification, options)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def _install_wheel( # noqa: C901, PLR0915 function is too long
|
| 420 |
+
name: str,
|
| 421 |
+
wheel_zip: ZipFile,
|
| 422 |
+
wheel_path: str,
|
| 423 |
+
scheme: Scheme,
|
| 424 |
+
pycompile: bool = True,
|
| 425 |
+
warn_script_location: bool = True,
|
| 426 |
+
direct_url: Optional[DirectUrl] = None,
|
| 427 |
+
requested: bool = False,
|
| 428 |
+
) -> None:
|
| 429 |
+
"""Install a wheel.
|
| 430 |
+
|
| 431 |
+
:param name: Name of the project to install
|
| 432 |
+
:param wheel_zip: open ZipFile for wheel being installed
|
| 433 |
+
:param scheme: Distutils scheme dictating the install directories
|
| 434 |
+
:param req_description: String used in place of the requirement, for
|
| 435 |
+
logging
|
| 436 |
+
:param pycompile: Whether to byte-compile installed Python files
|
| 437 |
+
:param warn_script_location: Whether to check that scripts are installed
|
| 438 |
+
into a directory on PATH
|
| 439 |
+
:raises UnsupportedWheel:
|
| 440 |
+
* when the directory holds an unpacked wheel with incompatible
|
| 441 |
+
Wheel-Version
|
| 442 |
+
* when the .dist-info dir does not match the wheel
|
| 443 |
+
"""
|
| 444 |
+
info_dir, metadata = parse_wheel(wheel_zip, name)
|
| 445 |
+
|
| 446 |
+
if wheel_root_is_purelib(metadata):
|
| 447 |
+
lib_dir = scheme.purelib
|
| 448 |
+
else:
|
| 449 |
+
lib_dir = scheme.platlib
|
| 450 |
+
|
| 451 |
+
# Record details of the files moved
|
| 452 |
+
# installed = files copied from the wheel to the destination
|
| 453 |
+
# changed = files changed while installing (scripts #! line typically)
|
| 454 |
+
# generated = files newly generated during the install (script wrappers)
|
| 455 |
+
installed: Dict[RecordPath, RecordPath] = {}
|
| 456 |
+
changed: Set[RecordPath] = set()
|
| 457 |
+
generated: List[str] = []
|
| 458 |
+
|
| 459 |
+
def record_installed(
|
| 460 |
+
srcfile: RecordPath, destfile: str, modified: bool = False
|
| 461 |
+
) -> None:
|
| 462 |
+
"""Map archive RECORD paths to installation RECORD paths."""
|
| 463 |
+
newpath = _fs_to_record_path(destfile, lib_dir)
|
| 464 |
+
installed[srcfile] = newpath
|
| 465 |
+
if modified:
|
| 466 |
+
changed.add(newpath)
|
| 467 |
+
|
| 468 |
+
def is_dir_path(path: RecordPath) -> bool:
|
| 469 |
+
return path.endswith("/")
|
| 470 |
+
|
| 471 |
+
def assert_no_path_traversal(dest_dir_path: str, target_path: str) -> None:
|
| 472 |
+
if not is_within_directory(dest_dir_path, target_path):
|
| 473 |
+
message = (
|
| 474 |
+
"The wheel {!r} has a file {!r} trying to install"
|
| 475 |
+
" outside the target directory {!r}"
|
| 476 |
+
)
|
| 477 |
+
raise InstallationError(
|
| 478 |
+
message.format(wheel_path, target_path, dest_dir_path)
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
def root_scheme_file_maker(
|
| 482 |
+
zip_file: ZipFile, dest: str
|
| 483 |
+
) -> Callable[[RecordPath], "File"]:
|
| 484 |
+
def make_root_scheme_file(record_path: RecordPath) -> "File":
|
| 485 |
+
normed_path = os.path.normpath(record_path)
|
| 486 |
+
dest_path = os.path.join(dest, normed_path)
|
| 487 |
+
assert_no_path_traversal(dest, dest_path)
|
| 488 |
+
return ZipBackedFile(record_path, dest_path, zip_file)
|
| 489 |
+
|
| 490 |
+
return make_root_scheme_file
|
| 491 |
+
|
| 492 |
+
def data_scheme_file_maker(
|
| 493 |
+
zip_file: ZipFile, scheme: Scheme
|
| 494 |
+
) -> Callable[[RecordPath], "File"]:
|
| 495 |
+
scheme_paths = {key: getattr(scheme, key) for key in SCHEME_KEYS}
|
| 496 |
+
|
| 497 |
+
def make_data_scheme_file(record_path: RecordPath) -> "File":
|
| 498 |
+
normed_path = os.path.normpath(record_path)
|
| 499 |
+
try:
|
| 500 |
+
_, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2)
|
| 501 |
+
except ValueError:
|
| 502 |
+
message = (
|
| 503 |
+
f"Unexpected file in {wheel_path}: {record_path!r}. .data directory"
|
| 504 |
+
" contents should be named like: '<scheme key>/<path>'."
|
| 505 |
+
)
|
| 506 |
+
raise InstallationError(message)
|
| 507 |
+
|
| 508 |
+
try:
|
| 509 |
+
scheme_path = scheme_paths[scheme_key]
|
| 510 |
+
except KeyError:
|
| 511 |
+
valid_scheme_keys = ", ".join(sorted(scheme_paths))
|
| 512 |
+
message = (
|
| 513 |
+
f"Unknown scheme key used in {wheel_path}: {scheme_key} "
|
| 514 |
+
f"(for file {record_path!r}). .data directory contents "
|
| 515 |
+
f"should be in subdirectories named with a valid scheme "
|
| 516 |
+
f"key ({valid_scheme_keys})"
|
| 517 |
+
)
|
| 518 |
+
raise InstallationError(message)
|
| 519 |
+
|
| 520 |
+
dest_path = os.path.join(scheme_path, dest_subpath)
|
| 521 |
+
assert_no_path_traversal(scheme_path, dest_path)
|
| 522 |
+
return ZipBackedFile(record_path, dest_path, zip_file)
|
| 523 |
+
|
| 524 |
+
return make_data_scheme_file
|
| 525 |
+
|
| 526 |
+
def is_data_scheme_path(path: RecordPath) -> bool:
|
| 527 |
+
return path.split("/", 1)[0].endswith(".data")
|
| 528 |
+
|
| 529 |
+
paths = cast(List[RecordPath], wheel_zip.namelist())
|
| 530 |
+
file_paths = filterfalse(is_dir_path, paths)
|
| 531 |
+
root_scheme_paths, data_scheme_paths = partition(is_data_scheme_path, file_paths)
|
| 532 |
+
|
| 533 |
+
make_root_scheme_file = root_scheme_file_maker(wheel_zip, lib_dir)
|
| 534 |
+
files: Iterator[File] = map(make_root_scheme_file, root_scheme_paths)
|
| 535 |
+
|
| 536 |
+
def is_script_scheme_path(path: RecordPath) -> bool:
|
| 537 |
+
parts = path.split("/", 2)
|
| 538 |
+
return len(parts) > 2 and parts[0].endswith(".data") and parts[1] == "scripts"
|
| 539 |
+
|
| 540 |
+
other_scheme_paths, script_scheme_paths = partition(
|
| 541 |
+
is_script_scheme_path, data_scheme_paths
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
make_data_scheme_file = data_scheme_file_maker(wheel_zip, scheme)
|
| 545 |
+
other_scheme_files = map(make_data_scheme_file, other_scheme_paths)
|
| 546 |
+
files = chain(files, other_scheme_files)
|
| 547 |
+
|
| 548 |
+
# Get the defined entry points
|
| 549 |
+
distribution = get_wheel_distribution(
|
| 550 |
+
FilesystemWheel(wheel_path),
|
| 551 |
+
canonicalize_name(name),
|
| 552 |
+
)
|
| 553 |
+
console, gui = get_entrypoints(distribution)
|
| 554 |
+
|
| 555 |
+
def is_entrypoint_wrapper(file: "File") -> bool:
|
| 556 |
+
# EP, EP.exe and EP-script.py are scripts generated for
|
| 557 |
+
# entry point EP by setuptools
|
| 558 |
+
path = file.dest_path
|
| 559 |
+
name = os.path.basename(path)
|
| 560 |
+
if name.lower().endswith(".exe"):
|
| 561 |
+
matchname = name[:-4]
|
| 562 |
+
elif name.lower().endswith("-script.py"):
|
| 563 |
+
matchname = name[:-10]
|
| 564 |
+
elif name.lower().endswith(".pya"):
|
| 565 |
+
matchname = name[:-4]
|
| 566 |
+
else:
|
| 567 |
+
matchname = name
|
| 568 |
+
# Ignore setuptools-generated scripts
|
| 569 |
+
return matchname in console or matchname in gui
|
| 570 |
+
|
| 571 |
+
script_scheme_files: Iterator[File] = map(
|
| 572 |
+
make_data_scheme_file, script_scheme_paths
|
| 573 |
+
)
|
| 574 |
+
script_scheme_files = filterfalse(is_entrypoint_wrapper, script_scheme_files)
|
| 575 |
+
script_scheme_files = map(ScriptFile, script_scheme_files)
|
| 576 |
+
files = chain(files, script_scheme_files)
|
| 577 |
+
|
| 578 |
+
existing_parents = set()
|
| 579 |
+
for file in files:
|
| 580 |
+
# directory creation is lazy and after file filtering
|
| 581 |
+
# to ensure we don't install empty dirs; empty dirs can't be
|
| 582 |
+
# uninstalled.
|
| 583 |
+
parent_dir = os.path.dirname(file.dest_path)
|
| 584 |
+
if parent_dir not in existing_parents:
|
| 585 |
+
ensure_dir(parent_dir)
|
| 586 |
+
existing_parents.add(parent_dir)
|
| 587 |
+
file.save()
|
| 588 |
+
record_installed(file.src_record_path, file.dest_path, file.changed)
|
| 589 |
+
|
| 590 |
+
def pyc_source_file_paths() -> Generator[str, None, None]:
|
| 591 |
+
# We de-duplicate installation paths, since there can be overlap (e.g.
|
| 592 |
+
# file in .data maps to same location as file in wheel root).
|
| 593 |
+
# Sorting installation paths makes it easier to reproduce and debug
|
| 594 |
+
# issues related to permissions on existing files.
|
| 595 |
+
for installed_path in sorted(set(installed.values())):
|
| 596 |
+
full_installed_path = os.path.join(lib_dir, installed_path)
|
| 597 |
+
if not os.path.isfile(full_installed_path):
|
| 598 |
+
continue
|
| 599 |
+
if not full_installed_path.endswith(".py"):
|
| 600 |
+
continue
|
| 601 |
+
yield full_installed_path
|
| 602 |
+
|
| 603 |
+
def pyc_output_path(path: str) -> str:
|
| 604 |
+
"""Return the path the pyc file would have been written to."""
|
| 605 |
+
return importlib.util.cache_from_source(path)
|
| 606 |
+
|
| 607 |
+
# Compile all of the pyc files for the installed files
|
| 608 |
+
if pycompile:
|
| 609 |
+
with contextlib.redirect_stdout(
|
| 610 |
+
StreamWrapper.from_stream(sys.stdout)
|
| 611 |
+
) as stdout:
|
| 612 |
+
with warnings.catch_warnings():
|
| 613 |
+
warnings.filterwarnings("ignore")
|
| 614 |
+
for path in pyc_source_file_paths():
|
| 615 |
+
success = compileall.compile_file(path, force=True, quiet=True)
|
| 616 |
+
if success:
|
| 617 |
+
pyc_path = pyc_output_path(path)
|
| 618 |
+
assert os.path.exists(pyc_path)
|
| 619 |
+
pyc_record_path = cast(
|
| 620 |
+
"RecordPath", pyc_path.replace(os.path.sep, "/")
|
| 621 |
+
)
|
| 622 |
+
record_installed(pyc_record_path, pyc_path)
|
| 623 |
+
logger.debug(stdout.getvalue())
|
| 624 |
+
|
| 625 |
+
maker = PipScriptMaker(None, scheme.scripts)
|
| 626 |
+
|
| 627 |
+
# Ensure old scripts are overwritten.
|
| 628 |
+
# See https://github.com/pypa/pip/issues/1800
|
| 629 |
+
maker.clobber = True
|
| 630 |
+
|
| 631 |
+
# Ensure we don't generate any variants for scripts because this is almost
|
| 632 |
+
# never what somebody wants.
|
| 633 |
+
# See https://bitbucket.org/pypa/distlib/issue/35/
|
| 634 |
+
maker.variants = {""}
|
| 635 |
+
|
| 636 |
+
# This is required because otherwise distlib creates scripts that are not
|
| 637 |
+
# executable.
|
| 638 |
+
# See https://bitbucket.org/pypa/distlib/issue/32/
|
| 639 |
+
maker.set_mode = True
|
| 640 |
+
|
| 641 |
+
# Generate the console and GUI entry points specified in the wheel
|
| 642 |
+
scripts_to_generate = get_console_script_specs(console)
|
| 643 |
+
|
| 644 |
+
gui_scripts_to_generate = list(starmap("{} = {}".format, gui.items()))
|
| 645 |
+
|
| 646 |
+
generated_console_scripts = maker.make_multiple(scripts_to_generate)
|
| 647 |
+
generated.extend(generated_console_scripts)
|
| 648 |
+
|
| 649 |
+
generated.extend(maker.make_multiple(gui_scripts_to_generate, {"gui": True}))
|
| 650 |
+
|
| 651 |
+
if warn_script_location:
|
| 652 |
+
msg = message_about_scripts_not_on_PATH(generated_console_scripts)
|
| 653 |
+
if msg is not None:
|
| 654 |
+
logger.warning(msg)
|
| 655 |
+
|
| 656 |
+
generated_file_mode = 0o666 & ~current_umask()
|
| 657 |
+
|
| 658 |
+
@contextlib.contextmanager
|
| 659 |
+
def _generate_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, None]:
|
| 660 |
+
with adjacent_tmp_file(path, **kwargs) as f:
|
| 661 |
+
yield f
|
| 662 |
+
os.chmod(f.name, generated_file_mode)
|
| 663 |
+
replace(f.name, path)
|
| 664 |
+
|
| 665 |
+
dest_info_dir = os.path.join(lib_dir, info_dir)
|
| 666 |
+
|
| 667 |
+
# Record pip as the installer
|
| 668 |
+
installer_path = os.path.join(dest_info_dir, "INSTALLER")
|
| 669 |
+
with _generate_file(installer_path) as installer_file:
|
| 670 |
+
installer_file.write(b"pip\n")
|
| 671 |
+
generated.append(installer_path)
|
| 672 |
+
|
| 673 |
+
# Record the PEP 610 direct URL reference
|
| 674 |
+
if direct_url is not None:
|
| 675 |
+
direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME)
|
| 676 |
+
with _generate_file(direct_url_path) as direct_url_file:
|
| 677 |
+
direct_url_file.write(direct_url.to_json().encode("utf-8"))
|
| 678 |
+
generated.append(direct_url_path)
|
| 679 |
+
|
| 680 |
+
# Record the REQUESTED file
|
| 681 |
+
if requested:
|
| 682 |
+
requested_path = os.path.join(dest_info_dir, "REQUESTED")
|
| 683 |
+
with open(requested_path, "wb"):
|
| 684 |
+
pass
|
| 685 |
+
generated.append(requested_path)
|
| 686 |
+
|
| 687 |
+
record_text = distribution.read_text("RECORD")
|
| 688 |
+
record_rows = list(csv.reader(record_text.splitlines()))
|
| 689 |
+
|
| 690 |
+
rows = get_csv_rows_for_installed(
|
| 691 |
+
record_rows,
|
| 692 |
+
installed=installed,
|
| 693 |
+
changed=changed,
|
| 694 |
+
generated=generated,
|
| 695 |
+
lib_dir=lib_dir,
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
# Record details of all files installed
|
| 699 |
+
record_path = os.path.join(dest_info_dir, "RECORD")
|
| 700 |
+
|
| 701 |
+
with _generate_file(record_path, **csv_io_kwargs("w")) as record_file:
|
| 702 |
+
# Explicitly cast to typing.IO[str] as a workaround for the mypy error:
|
| 703 |
+
# "writer" has incompatible type "BinaryIO"; expected "_Writer"
|
| 704 |
+
writer = csv.writer(cast("IO[str]", record_file))
|
| 705 |
+
writer.writerows(_normalized_outrows(rows))
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
@contextlib.contextmanager
|
| 709 |
+
def req_error_context(req_description: str) -> Generator[None, None, None]:
|
| 710 |
+
try:
|
| 711 |
+
yield
|
| 712 |
+
except InstallationError as e:
|
| 713 |
+
message = f"For req: {req_description}. {e.args[0]}"
|
| 714 |
+
raise InstallationError(message) from e
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
def install_wheel(
|
| 718 |
+
name: str,
|
| 719 |
+
wheel_path: str,
|
| 720 |
+
scheme: Scheme,
|
| 721 |
+
req_description: str,
|
| 722 |
+
pycompile: bool = True,
|
| 723 |
+
warn_script_location: bool = True,
|
| 724 |
+
direct_url: Optional[DirectUrl] = None,
|
| 725 |
+
requested: bool = False,
|
| 726 |
+
) -> None:
|
| 727 |
+
with ZipFile(wheel_path, allowZip64=True) as z:
|
| 728 |
+
with req_error_context(req_description):
|
| 729 |
+
_install_wheel(
|
| 730 |
+
name=name,
|
| 731 |
+
wheel_zip=z,
|
| 732 |
+
wheel_path=wheel_path,
|
| 733 |
+
scheme=scheme,
|
| 734 |
+
pycompile=pycompile,
|
| 735 |
+
warn_script_location=warn_script_location,
|
| 736 |
+
direct_url=direct_url,
|
| 737 |
+
requested=requested,
|
| 738 |
+
)
|
.env/lib/python3.13/site-packages/pip/_internal/operations/prepare.py
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prepares a distribution for installation"""
|
| 2 |
+
|
| 3 |
+
# The following comment should be removed at some point in the future.
|
| 4 |
+
# mypy: strict-optional=False
|
| 5 |
+
|
| 6 |
+
import mimetypes
|
| 7 |
+
import os
|
| 8 |
+
import shutil
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Dict, Iterable, List, Optional
|
| 12 |
+
|
| 13 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 14 |
+
|
| 15 |
+
from pip._internal.distributions import make_distribution_for_install_requirement
|
| 16 |
+
from pip._internal.distributions.installed import InstalledDistribution
|
| 17 |
+
from pip._internal.exceptions import (
|
| 18 |
+
DirectoryUrlHashUnsupported,
|
| 19 |
+
HashMismatch,
|
| 20 |
+
HashUnpinned,
|
| 21 |
+
InstallationError,
|
| 22 |
+
MetadataInconsistent,
|
| 23 |
+
NetworkConnectionError,
|
| 24 |
+
VcsHashUnsupported,
|
| 25 |
+
)
|
| 26 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 27 |
+
from pip._internal.metadata import BaseDistribution, get_metadata_distribution
|
| 28 |
+
from pip._internal.models.direct_url import ArchiveInfo
|
| 29 |
+
from pip._internal.models.link import Link
|
| 30 |
+
from pip._internal.models.wheel import Wheel
|
| 31 |
+
from pip._internal.network.download import BatchDownloader, Downloader
|
| 32 |
+
from pip._internal.network.lazy_wheel import (
|
| 33 |
+
HTTPRangeRequestUnsupported,
|
| 34 |
+
dist_from_wheel_url,
|
| 35 |
+
)
|
| 36 |
+
from pip._internal.network.session import PipSession
|
| 37 |
+
from pip._internal.operations.build.build_tracker import BuildTracker
|
| 38 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 39 |
+
from pip._internal.utils._log import getLogger
|
| 40 |
+
from pip._internal.utils.direct_url_helpers import (
|
| 41 |
+
direct_url_for_editable,
|
| 42 |
+
direct_url_from_link,
|
| 43 |
+
)
|
| 44 |
+
from pip._internal.utils.hashes import Hashes, MissingHashes
|
| 45 |
+
from pip._internal.utils.logging import indent_log
|
| 46 |
+
from pip._internal.utils.misc import (
|
| 47 |
+
display_path,
|
| 48 |
+
hash_file,
|
| 49 |
+
hide_url,
|
| 50 |
+
redact_auth_from_requirement,
|
| 51 |
+
)
|
| 52 |
+
from pip._internal.utils.temp_dir import TempDirectory
|
| 53 |
+
from pip._internal.utils.unpacking import unpack_file
|
| 54 |
+
from pip._internal.vcs import vcs
|
| 55 |
+
|
| 56 |
+
logger = getLogger(__name__)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _get_prepared_distribution(
|
| 60 |
+
req: InstallRequirement,
|
| 61 |
+
build_tracker: BuildTracker,
|
| 62 |
+
finder: PackageFinder,
|
| 63 |
+
build_isolation: bool,
|
| 64 |
+
check_build_deps: bool,
|
| 65 |
+
) -> BaseDistribution:
|
| 66 |
+
"""Prepare a distribution for installation."""
|
| 67 |
+
abstract_dist = make_distribution_for_install_requirement(req)
|
| 68 |
+
tracker_id = abstract_dist.build_tracker_id
|
| 69 |
+
if tracker_id is not None:
|
| 70 |
+
with build_tracker.track(req, tracker_id):
|
| 71 |
+
abstract_dist.prepare_distribution_metadata(
|
| 72 |
+
finder, build_isolation, check_build_deps
|
| 73 |
+
)
|
| 74 |
+
return abstract_dist.get_metadata_distribution()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
|
| 78 |
+
vcs_backend = vcs.get_backend_for_scheme(link.scheme)
|
| 79 |
+
assert vcs_backend is not None
|
| 80 |
+
vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
@dataclass
|
| 84 |
+
class File:
|
| 85 |
+
path: str
|
| 86 |
+
content_type: Optional[str] = None
|
| 87 |
+
|
| 88 |
+
def __post_init__(self) -> None:
|
| 89 |
+
if self.content_type is None:
|
| 90 |
+
# Try to guess the file's MIME type. If the system MIME tables
|
| 91 |
+
# can't be loaded, give up.
|
| 92 |
+
try:
|
| 93 |
+
self.content_type = mimetypes.guess_type(self.path)[0]
|
| 94 |
+
except OSError:
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def get_http_url(
|
| 99 |
+
link: Link,
|
| 100 |
+
download: Downloader,
|
| 101 |
+
download_dir: Optional[str] = None,
|
| 102 |
+
hashes: Optional[Hashes] = None,
|
| 103 |
+
) -> File:
|
| 104 |
+
temp_dir = TempDirectory(kind="unpack", globally_managed=True)
|
| 105 |
+
# If a download dir is specified, is the file already downloaded there?
|
| 106 |
+
already_downloaded_path = None
|
| 107 |
+
if download_dir:
|
| 108 |
+
already_downloaded_path = _check_download_dir(link, download_dir, hashes)
|
| 109 |
+
|
| 110 |
+
if already_downloaded_path:
|
| 111 |
+
from_path = already_downloaded_path
|
| 112 |
+
content_type = None
|
| 113 |
+
else:
|
| 114 |
+
# let's download to a tmp dir
|
| 115 |
+
from_path, content_type = download(link, temp_dir.path)
|
| 116 |
+
if hashes:
|
| 117 |
+
hashes.check_against_path(from_path)
|
| 118 |
+
|
| 119 |
+
return File(from_path, content_type)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def get_file_url(
|
| 123 |
+
link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None
|
| 124 |
+
) -> File:
|
| 125 |
+
"""Get file and optionally check its hash."""
|
| 126 |
+
# If a download dir is specified, is the file already there and valid?
|
| 127 |
+
already_downloaded_path = None
|
| 128 |
+
if download_dir:
|
| 129 |
+
already_downloaded_path = _check_download_dir(link, download_dir, hashes)
|
| 130 |
+
|
| 131 |
+
if already_downloaded_path:
|
| 132 |
+
from_path = already_downloaded_path
|
| 133 |
+
else:
|
| 134 |
+
from_path = link.file_path
|
| 135 |
+
|
| 136 |
+
# If --require-hashes is off, `hashes` is either empty, the
|
| 137 |
+
# link's embedded hash, or MissingHashes; it is required to
|
| 138 |
+
# match. If --require-hashes is on, we are satisfied by any
|
| 139 |
+
# hash in `hashes` matching: a URL-based or an option-based
|
| 140 |
+
# one; no internet-sourced hash will be in `hashes`.
|
| 141 |
+
if hashes:
|
| 142 |
+
hashes.check_against_path(from_path)
|
| 143 |
+
return File(from_path, None)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def unpack_url(
|
| 147 |
+
link: Link,
|
| 148 |
+
location: str,
|
| 149 |
+
download: Downloader,
|
| 150 |
+
verbosity: int,
|
| 151 |
+
download_dir: Optional[str] = None,
|
| 152 |
+
hashes: Optional[Hashes] = None,
|
| 153 |
+
) -> Optional[File]:
|
| 154 |
+
"""Unpack link into location, downloading if required.
|
| 155 |
+
|
| 156 |
+
:param hashes: A Hashes object, one of whose embedded hashes must match,
|
| 157 |
+
or HashMismatch will be raised. If the Hashes is empty, no matches are
|
| 158 |
+
required, and unhashable types of requirements (like VCS ones, which
|
| 159 |
+
would ordinarily raise HashUnsupported) are allowed.
|
| 160 |
+
"""
|
| 161 |
+
# non-editable vcs urls
|
| 162 |
+
if link.is_vcs:
|
| 163 |
+
unpack_vcs_link(link, location, verbosity=verbosity)
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
assert not link.is_existing_dir()
|
| 167 |
+
|
| 168 |
+
# file urls
|
| 169 |
+
if link.is_file:
|
| 170 |
+
file = get_file_url(link, download_dir, hashes=hashes)
|
| 171 |
+
|
| 172 |
+
# http urls
|
| 173 |
+
else:
|
| 174 |
+
file = get_http_url(
|
| 175 |
+
link,
|
| 176 |
+
download,
|
| 177 |
+
download_dir,
|
| 178 |
+
hashes=hashes,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# unpack the archive to the build dir location. even when only downloading
|
| 182 |
+
# archives, they have to be unpacked to parse dependencies, except wheels
|
| 183 |
+
if not link.is_wheel:
|
| 184 |
+
unpack_file(file.path, location, file.content_type)
|
| 185 |
+
|
| 186 |
+
return file
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _check_download_dir(
|
| 190 |
+
link: Link,
|
| 191 |
+
download_dir: str,
|
| 192 |
+
hashes: Optional[Hashes],
|
| 193 |
+
warn_on_hash_mismatch: bool = True,
|
| 194 |
+
) -> Optional[str]:
|
| 195 |
+
"""Check download_dir for previously downloaded file with correct hash
|
| 196 |
+
If a correct file is found return its path else None
|
| 197 |
+
"""
|
| 198 |
+
download_path = os.path.join(download_dir, link.filename)
|
| 199 |
+
|
| 200 |
+
if not os.path.exists(download_path):
|
| 201 |
+
return None
|
| 202 |
+
|
| 203 |
+
# If already downloaded, does its hash match?
|
| 204 |
+
logger.info("File was already downloaded %s", download_path)
|
| 205 |
+
if hashes:
|
| 206 |
+
try:
|
| 207 |
+
hashes.check_against_path(download_path)
|
| 208 |
+
except HashMismatch:
|
| 209 |
+
if warn_on_hash_mismatch:
|
| 210 |
+
logger.warning(
|
| 211 |
+
"Previously-downloaded file %s has bad hash. Re-downloading.",
|
| 212 |
+
download_path,
|
| 213 |
+
)
|
| 214 |
+
os.unlink(download_path)
|
| 215 |
+
return None
|
| 216 |
+
return download_path
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class RequirementPreparer:
|
| 220 |
+
"""Prepares a Requirement"""
|
| 221 |
+
|
| 222 |
+
def __init__(
|
| 223 |
+
self,
|
| 224 |
+
build_dir: str,
|
| 225 |
+
download_dir: Optional[str],
|
| 226 |
+
src_dir: str,
|
| 227 |
+
build_isolation: bool,
|
| 228 |
+
check_build_deps: bool,
|
| 229 |
+
build_tracker: BuildTracker,
|
| 230 |
+
session: PipSession,
|
| 231 |
+
progress_bar: str,
|
| 232 |
+
finder: PackageFinder,
|
| 233 |
+
require_hashes: bool,
|
| 234 |
+
use_user_site: bool,
|
| 235 |
+
lazy_wheel: bool,
|
| 236 |
+
verbosity: int,
|
| 237 |
+
legacy_resolver: bool,
|
| 238 |
+
resume_retries: int,
|
| 239 |
+
) -> None:
|
| 240 |
+
super().__init__()
|
| 241 |
+
|
| 242 |
+
self.src_dir = src_dir
|
| 243 |
+
self.build_dir = build_dir
|
| 244 |
+
self.build_tracker = build_tracker
|
| 245 |
+
self._session = session
|
| 246 |
+
self._download = Downloader(session, progress_bar, resume_retries)
|
| 247 |
+
self._batch_download = BatchDownloader(session, progress_bar, resume_retries)
|
| 248 |
+
self.finder = finder
|
| 249 |
+
|
| 250 |
+
# Where still-packed archives should be written to. If None, they are
|
| 251 |
+
# not saved, and are deleted immediately after unpacking.
|
| 252 |
+
self.download_dir = download_dir
|
| 253 |
+
|
| 254 |
+
# Is build isolation allowed?
|
| 255 |
+
self.build_isolation = build_isolation
|
| 256 |
+
|
| 257 |
+
# Should check build dependencies?
|
| 258 |
+
self.check_build_deps = check_build_deps
|
| 259 |
+
|
| 260 |
+
# Should hash-checking be required?
|
| 261 |
+
self.require_hashes = require_hashes
|
| 262 |
+
|
| 263 |
+
# Should install in user site-packages?
|
| 264 |
+
self.use_user_site = use_user_site
|
| 265 |
+
|
| 266 |
+
# Should wheels be downloaded lazily?
|
| 267 |
+
self.use_lazy_wheel = lazy_wheel
|
| 268 |
+
|
| 269 |
+
# How verbose should underlying tooling be?
|
| 270 |
+
self.verbosity = verbosity
|
| 271 |
+
|
| 272 |
+
# Are we using the legacy resolver?
|
| 273 |
+
self.legacy_resolver = legacy_resolver
|
| 274 |
+
|
| 275 |
+
# Memoized downloaded files, as mapping of url: path.
|
| 276 |
+
self._downloaded: Dict[str, str] = {}
|
| 277 |
+
|
| 278 |
+
# Previous "header" printed for a link-based InstallRequirement
|
| 279 |
+
self._previous_requirement_header = ("", "")
|
| 280 |
+
|
| 281 |
+
def _log_preparing_link(self, req: InstallRequirement) -> None:
|
| 282 |
+
"""Provide context for the requirement being prepared."""
|
| 283 |
+
if req.link.is_file and not req.is_wheel_from_cache:
|
| 284 |
+
message = "Processing %s"
|
| 285 |
+
information = str(display_path(req.link.file_path))
|
| 286 |
+
else:
|
| 287 |
+
message = "Collecting %s"
|
| 288 |
+
information = redact_auth_from_requirement(req.req) if req.req else str(req)
|
| 289 |
+
|
| 290 |
+
# If we used req.req, inject requirement source if available (this
|
| 291 |
+
# would already be included if we used req directly)
|
| 292 |
+
if req.req and req.comes_from:
|
| 293 |
+
if isinstance(req.comes_from, str):
|
| 294 |
+
comes_from: Optional[str] = req.comes_from
|
| 295 |
+
else:
|
| 296 |
+
comes_from = req.comes_from.from_path()
|
| 297 |
+
if comes_from:
|
| 298 |
+
information += f" (from {comes_from})"
|
| 299 |
+
|
| 300 |
+
if (message, information) != self._previous_requirement_header:
|
| 301 |
+
self._previous_requirement_header = (message, information)
|
| 302 |
+
logger.info(message, information)
|
| 303 |
+
|
| 304 |
+
if req.is_wheel_from_cache:
|
| 305 |
+
with indent_log():
|
| 306 |
+
logger.info("Using cached %s", req.link.filename)
|
| 307 |
+
|
| 308 |
+
def _ensure_link_req_src_dir(
|
| 309 |
+
self, req: InstallRequirement, parallel_builds: bool
|
| 310 |
+
) -> None:
|
| 311 |
+
"""Ensure source_dir of a linked InstallRequirement."""
|
| 312 |
+
# Since source_dir is only set for editable requirements.
|
| 313 |
+
if req.link.is_wheel:
|
| 314 |
+
# We don't need to unpack wheels, so no need for a source
|
| 315 |
+
# directory.
|
| 316 |
+
return
|
| 317 |
+
assert req.source_dir is None
|
| 318 |
+
if req.link.is_existing_dir():
|
| 319 |
+
# build local directories in-tree
|
| 320 |
+
req.source_dir = req.link.file_path
|
| 321 |
+
return
|
| 322 |
+
|
| 323 |
+
# We always delete unpacked sdists after pip runs.
|
| 324 |
+
req.ensure_has_source_dir(
|
| 325 |
+
self.build_dir,
|
| 326 |
+
autodelete=True,
|
| 327 |
+
parallel_builds=parallel_builds,
|
| 328 |
+
)
|
| 329 |
+
req.ensure_pristine_source_checkout()
|
| 330 |
+
|
| 331 |
+
def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
|
| 332 |
+
# By the time this is called, the requirement's link should have
|
| 333 |
+
# been checked so we can tell what kind of requirements req is
|
| 334 |
+
# and raise some more informative errors than otherwise.
|
| 335 |
+
# (For example, we can raise VcsHashUnsupported for a VCS URL
|
| 336 |
+
# rather than HashMissing.)
|
| 337 |
+
if not self.require_hashes:
|
| 338 |
+
return req.hashes(trust_internet=True)
|
| 339 |
+
|
| 340 |
+
# We could check these first 2 conditions inside unpack_url
|
| 341 |
+
# and save repetition of conditions, but then we would
|
| 342 |
+
# report less-useful error messages for unhashable
|
| 343 |
+
# requirements, complaining that there's no hash provided.
|
| 344 |
+
if req.link.is_vcs:
|
| 345 |
+
raise VcsHashUnsupported()
|
| 346 |
+
if req.link.is_existing_dir():
|
| 347 |
+
raise DirectoryUrlHashUnsupported()
|
| 348 |
+
|
| 349 |
+
# Unpinned packages are asking for trouble when a new version
|
| 350 |
+
# is uploaded. This isn't a security check, but it saves users
|
| 351 |
+
# a surprising hash mismatch in the future.
|
| 352 |
+
# file:/// URLs aren't pinnable, so don't complain about them
|
| 353 |
+
# not being pinned.
|
| 354 |
+
if not req.is_direct and not req.is_pinned:
|
| 355 |
+
raise HashUnpinned()
|
| 356 |
+
|
| 357 |
+
# If known-good hashes are missing for this requirement,
|
| 358 |
+
# shim it with a facade object that will provoke hash
|
| 359 |
+
# computation and then raise a HashMissing exception
|
| 360 |
+
# showing the user what the hash should be.
|
| 361 |
+
return req.hashes(trust_internet=False) or MissingHashes()
|
| 362 |
+
|
| 363 |
+
def _fetch_metadata_only(
|
| 364 |
+
self,
|
| 365 |
+
req: InstallRequirement,
|
| 366 |
+
) -> Optional[BaseDistribution]:
|
| 367 |
+
if self.legacy_resolver:
|
| 368 |
+
logger.debug(
|
| 369 |
+
"Metadata-only fetching is not used in the legacy resolver",
|
| 370 |
+
)
|
| 371 |
+
return None
|
| 372 |
+
if self.require_hashes:
|
| 373 |
+
logger.debug(
|
| 374 |
+
"Metadata-only fetching is not used as hash checking is required",
|
| 375 |
+
)
|
| 376 |
+
return None
|
| 377 |
+
# Try PEP 658 metadata first, then fall back to lazy wheel if unavailable.
|
| 378 |
+
return self._fetch_metadata_using_link_data_attr(
|
| 379 |
+
req
|
| 380 |
+
) or self._fetch_metadata_using_lazy_wheel(req.link)
|
| 381 |
+
|
| 382 |
+
def _fetch_metadata_using_link_data_attr(
|
| 383 |
+
self,
|
| 384 |
+
req: InstallRequirement,
|
| 385 |
+
) -> Optional[BaseDistribution]:
|
| 386 |
+
"""Fetch metadata from the data-dist-info-metadata attribute, if possible."""
|
| 387 |
+
# (1) Get the link to the metadata file, if provided by the backend.
|
| 388 |
+
metadata_link = req.link.metadata_link()
|
| 389 |
+
if metadata_link is None:
|
| 390 |
+
return None
|
| 391 |
+
assert req.req is not None
|
| 392 |
+
logger.verbose(
|
| 393 |
+
"Obtaining dependency information for %s from %s",
|
| 394 |
+
req.req,
|
| 395 |
+
metadata_link,
|
| 396 |
+
)
|
| 397 |
+
# (2) Download the contents of the METADATA file, separate from the dist itself.
|
| 398 |
+
metadata_file = get_http_url(
|
| 399 |
+
metadata_link,
|
| 400 |
+
self._download,
|
| 401 |
+
hashes=metadata_link.as_hashes(),
|
| 402 |
+
)
|
| 403 |
+
with open(metadata_file.path, "rb") as f:
|
| 404 |
+
metadata_contents = f.read()
|
| 405 |
+
# (3) Generate a dist just from those file contents.
|
| 406 |
+
metadata_dist = get_metadata_distribution(
|
| 407 |
+
metadata_contents,
|
| 408 |
+
req.link.filename,
|
| 409 |
+
req.req.name,
|
| 410 |
+
)
|
| 411 |
+
# (4) Ensure the Name: field from the METADATA file matches the name from the
|
| 412 |
+
# install requirement.
|
| 413 |
+
#
|
| 414 |
+
# NB: raw_name will fall back to the name from the install requirement if
|
| 415 |
+
# the Name: field is not present, but it's noted in the raw_name docstring
|
| 416 |
+
# that that should NEVER happen anyway.
|
| 417 |
+
if canonicalize_name(metadata_dist.raw_name) != canonicalize_name(req.req.name):
|
| 418 |
+
raise MetadataInconsistent(
|
| 419 |
+
req, "Name", req.req.name, metadata_dist.raw_name
|
| 420 |
+
)
|
| 421 |
+
return metadata_dist
|
| 422 |
+
|
| 423 |
+
def _fetch_metadata_using_lazy_wheel(
|
| 424 |
+
self,
|
| 425 |
+
link: Link,
|
| 426 |
+
) -> Optional[BaseDistribution]:
|
| 427 |
+
"""Fetch metadata using lazy wheel, if possible."""
|
| 428 |
+
# --use-feature=fast-deps must be provided.
|
| 429 |
+
if not self.use_lazy_wheel:
|
| 430 |
+
return None
|
| 431 |
+
if link.is_file or not link.is_wheel:
|
| 432 |
+
logger.debug(
|
| 433 |
+
"Lazy wheel is not used as %r does not point to a remote wheel",
|
| 434 |
+
link,
|
| 435 |
+
)
|
| 436 |
+
return None
|
| 437 |
+
|
| 438 |
+
wheel = Wheel(link.filename)
|
| 439 |
+
name = canonicalize_name(wheel.name)
|
| 440 |
+
logger.info(
|
| 441 |
+
"Obtaining dependency information from %s %s",
|
| 442 |
+
name,
|
| 443 |
+
wheel.version,
|
| 444 |
+
)
|
| 445 |
+
url = link.url.split("#", 1)[0]
|
| 446 |
+
try:
|
| 447 |
+
return dist_from_wheel_url(name, url, self._session)
|
| 448 |
+
except HTTPRangeRequestUnsupported:
|
| 449 |
+
logger.debug("%s does not support range requests", url)
|
| 450 |
+
return None
|
| 451 |
+
|
| 452 |
+
def _complete_partial_requirements(
|
| 453 |
+
self,
|
| 454 |
+
partially_downloaded_reqs: Iterable[InstallRequirement],
|
| 455 |
+
parallel_builds: bool = False,
|
| 456 |
+
) -> None:
|
| 457 |
+
"""Download any requirements which were only fetched by metadata."""
|
| 458 |
+
# Download to a temporary directory. These will be copied over as
|
| 459 |
+
# needed for downstream 'download', 'wheel', and 'install' commands.
|
| 460 |
+
temp_dir = TempDirectory(kind="unpack", globally_managed=True).path
|
| 461 |
+
|
| 462 |
+
# Map each link to the requirement that owns it. This allows us to set
|
| 463 |
+
# `req.local_file_path` on the appropriate requirement after passing
|
| 464 |
+
# all the links at once into BatchDownloader.
|
| 465 |
+
links_to_fully_download: Dict[Link, InstallRequirement] = {}
|
| 466 |
+
for req in partially_downloaded_reqs:
|
| 467 |
+
assert req.link
|
| 468 |
+
links_to_fully_download[req.link] = req
|
| 469 |
+
|
| 470 |
+
batch_download = self._batch_download(
|
| 471 |
+
links_to_fully_download.keys(),
|
| 472 |
+
temp_dir,
|
| 473 |
+
)
|
| 474 |
+
for link, (filepath, _) in batch_download:
|
| 475 |
+
logger.debug("Downloading link %s to %s", link, filepath)
|
| 476 |
+
req = links_to_fully_download[link]
|
| 477 |
+
# Record the downloaded file path so wheel reqs can extract a Distribution
|
| 478 |
+
# in .get_dist().
|
| 479 |
+
req.local_file_path = filepath
|
| 480 |
+
# Record that the file is downloaded so we don't do it again in
|
| 481 |
+
# _prepare_linked_requirement().
|
| 482 |
+
self._downloaded[req.link.url] = filepath
|
| 483 |
+
|
| 484 |
+
# If this is an sdist, we need to unpack it after downloading, but the
|
| 485 |
+
# .source_dir won't be set up until we are in _prepare_linked_requirement().
|
| 486 |
+
# Add the downloaded archive to the install requirement to unpack after
|
| 487 |
+
# preparing the source dir.
|
| 488 |
+
if not req.is_wheel:
|
| 489 |
+
req.needs_unpacked_archive(Path(filepath))
|
| 490 |
+
|
| 491 |
+
# This step is necessary to ensure all lazy wheels are processed
|
| 492 |
+
# successfully by the 'download', 'wheel', and 'install' commands.
|
| 493 |
+
for req in partially_downloaded_reqs:
|
| 494 |
+
self._prepare_linked_requirement(req, parallel_builds)
|
| 495 |
+
|
| 496 |
+
def prepare_linked_requirement(
|
| 497 |
+
self, req: InstallRequirement, parallel_builds: bool = False
|
| 498 |
+
) -> BaseDistribution:
|
| 499 |
+
"""Prepare a requirement to be obtained from req.link."""
|
| 500 |
+
assert req.link
|
| 501 |
+
self._log_preparing_link(req)
|
| 502 |
+
with indent_log():
|
| 503 |
+
# Check if the relevant file is already available
|
| 504 |
+
# in the download directory
|
| 505 |
+
file_path = None
|
| 506 |
+
if self.download_dir is not None and req.link.is_wheel:
|
| 507 |
+
hashes = self._get_linked_req_hashes(req)
|
| 508 |
+
file_path = _check_download_dir(
|
| 509 |
+
req.link,
|
| 510 |
+
self.download_dir,
|
| 511 |
+
hashes,
|
| 512 |
+
# When a locally built wheel has been found in cache, we don't warn
|
| 513 |
+
# about re-downloading when the already downloaded wheel hash does
|
| 514 |
+
# not match. This is because the hash must be checked against the
|
| 515 |
+
# original link, not the cached link. It that case the already
|
| 516 |
+
# downloaded file will be removed and re-fetched from cache (which
|
| 517 |
+
# implies a hash check against the cache entry's origin.json).
|
| 518 |
+
warn_on_hash_mismatch=not req.is_wheel_from_cache,
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
if file_path is not None:
|
| 522 |
+
# The file is already available, so mark it as downloaded
|
| 523 |
+
self._downloaded[req.link.url] = file_path
|
| 524 |
+
else:
|
| 525 |
+
# The file is not available, attempt to fetch only metadata
|
| 526 |
+
metadata_dist = self._fetch_metadata_only(req)
|
| 527 |
+
if metadata_dist is not None:
|
| 528 |
+
req.needs_more_preparation = True
|
| 529 |
+
return metadata_dist
|
| 530 |
+
|
| 531 |
+
# None of the optimizations worked, fully prepare the requirement
|
| 532 |
+
return self._prepare_linked_requirement(req, parallel_builds)
|
| 533 |
+
|
| 534 |
+
def prepare_linked_requirements_more(
|
| 535 |
+
self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False
|
| 536 |
+
) -> None:
|
| 537 |
+
"""Prepare linked requirements more, if needed."""
|
| 538 |
+
reqs = [req for req in reqs if req.needs_more_preparation]
|
| 539 |
+
for req in reqs:
|
| 540 |
+
# Determine if any of these requirements were already downloaded.
|
| 541 |
+
if self.download_dir is not None and req.link.is_wheel:
|
| 542 |
+
hashes = self._get_linked_req_hashes(req)
|
| 543 |
+
file_path = _check_download_dir(req.link, self.download_dir, hashes)
|
| 544 |
+
if file_path is not None:
|
| 545 |
+
self._downloaded[req.link.url] = file_path
|
| 546 |
+
req.needs_more_preparation = False
|
| 547 |
+
|
| 548 |
+
# Prepare requirements we found were already downloaded for some
|
| 549 |
+
# reason. The other downloads will be completed separately.
|
| 550 |
+
partially_downloaded_reqs: List[InstallRequirement] = []
|
| 551 |
+
for req in reqs:
|
| 552 |
+
if req.needs_more_preparation:
|
| 553 |
+
partially_downloaded_reqs.append(req)
|
| 554 |
+
else:
|
| 555 |
+
self._prepare_linked_requirement(req, parallel_builds)
|
| 556 |
+
|
| 557 |
+
# TODO: separate this part out from RequirementPreparer when the v1
|
| 558 |
+
# resolver can be removed!
|
| 559 |
+
self._complete_partial_requirements(
|
| 560 |
+
partially_downloaded_reqs,
|
| 561 |
+
parallel_builds=parallel_builds,
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
def _prepare_linked_requirement(
|
| 565 |
+
self, req: InstallRequirement, parallel_builds: bool
|
| 566 |
+
) -> BaseDistribution:
|
| 567 |
+
assert req.link
|
| 568 |
+
link = req.link
|
| 569 |
+
|
| 570 |
+
hashes = self._get_linked_req_hashes(req)
|
| 571 |
+
|
| 572 |
+
if hashes and req.is_wheel_from_cache:
|
| 573 |
+
assert req.download_info is not None
|
| 574 |
+
assert link.is_wheel
|
| 575 |
+
assert link.is_file
|
| 576 |
+
# We need to verify hashes, and we have found the requirement in the cache
|
| 577 |
+
# of locally built wheels.
|
| 578 |
+
if (
|
| 579 |
+
isinstance(req.download_info.info, ArchiveInfo)
|
| 580 |
+
and req.download_info.info.hashes
|
| 581 |
+
and hashes.has_one_of(req.download_info.info.hashes)
|
| 582 |
+
):
|
| 583 |
+
# At this point we know the requirement was built from a hashable source
|
| 584 |
+
# artifact, and we verified that the cache entry's hash of the original
|
| 585 |
+
# artifact matches one of the hashes we expect. We don't verify hashes
|
| 586 |
+
# against the cached wheel, because the wheel is not the original.
|
| 587 |
+
hashes = None
|
| 588 |
+
else:
|
| 589 |
+
logger.warning(
|
| 590 |
+
"The hashes of the source archive found in cache entry "
|
| 591 |
+
"don't match, ignoring cached built wheel "
|
| 592 |
+
"and re-downloading source."
|
| 593 |
+
)
|
| 594 |
+
req.link = req.cached_wheel_source_link
|
| 595 |
+
link = req.link
|
| 596 |
+
|
| 597 |
+
self._ensure_link_req_src_dir(req, parallel_builds)
|
| 598 |
+
|
| 599 |
+
if link.is_existing_dir():
|
| 600 |
+
local_file = None
|
| 601 |
+
elif link.url not in self._downloaded:
|
| 602 |
+
try:
|
| 603 |
+
local_file = unpack_url(
|
| 604 |
+
link,
|
| 605 |
+
req.source_dir,
|
| 606 |
+
self._download,
|
| 607 |
+
self.verbosity,
|
| 608 |
+
self.download_dir,
|
| 609 |
+
hashes,
|
| 610 |
+
)
|
| 611 |
+
except NetworkConnectionError as exc:
|
| 612 |
+
raise InstallationError(
|
| 613 |
+
f"Could not install requirement {req} because of HTTP "
|
| 614 |
+
f"error {exc} for URL {link}"
|
| 615 |
+
)
|
| 616 |
+
else:
|
| 617 |
+
file_path = self._downloaded[link.url]
|
| 618 |
+
if hashes:
|
| 619 |
+
hashes.check_against_path(file_path)
|
| 620 |
+
local_file = File(file_path, content_type=None)
|
| 621 |
+
|
| 622 |
+
# If download_info is set, we got it from the wheel cache.
|
| 623 |
+
if req.download_info is None:
|
| 624 |
+
# Editables don't go through this function (see
|
| 625 |
+
# prepare_editable_requirement).
|
| 626 |
+
assert not req.editable
|
| 627 |
+
req.download_info = direct_url_from_link(link, req.source_dir)
|
| 628 |
+
# Make sure we have a hash in download_info. If we got it as part of the
|
| 629 |
+
# URL, it will have been verified and we can rely on it. Otherwise we
|
| 630 |
+
# compute it from the downloaded file.
|
| 631 |
+
# FIXME: https://github.com/pypa/pip/issues/11943
|
| 632 |
+
if (
|
| 633 |
+
isinstance(req.download_info.info, ArchiveInfo)
|
| 634 |
+
and not req.download_info.info.hashes
|
| 635 |
+
and local_file
|
| 636 |
+
):
|
| 637 |
+
hash = hash_file(local_file.path)[0].hexdigest()
|
| 638 |
+
# We populate info.hash for backward compatibility.
|
| 639 |
+
# This will automatically populate info.hashes.
|
| 640 |
+
req.download_info.info.hash = f"sha256={hash}"
|
| 641 |
+
|
| 642 |
+
# For use in later processing,
|
| 643 |
+
# preserve the file path on the requirement.
|
| 644 |
+
if local_file:
|
| 645 |
+
req.local_file_path = local_file.path
|
| 646 |
+
|
| 647 |
+
dist = _get_prepared_distribution(
|
| 648 |
+
req,
|
| 649 |
+
self.build_tracker,
|
| 650 |
+
self.finder,
|
| 651 |
+
self.build_isolation,
|
| 652 |
+
self.check_build_deps,
|
| 653 |
+
)
|
| 654 |
+
return dist
|
| 655 |
+
|
| 656 |
+
def save_linked_requirement(self, req: InstallRequirement) -> None:
|
| 657 |
+
assert self.download_dir is not None
|
| 658 |
+
assert req.link is not None
|
| 659 |
+
link = req.link
|
| 660 |
+
if link.is_vcs or (link.is_existing_dir() and req.editable):
|
| 661 |
+
# Make a .zip of the source_dir we already created.
|
| 662 |
+
req.archive(self.download_dir)
|
| 663 |
+
return
|
| 664 |
+
|
| 665 |
+
if link.is_existing_dir():
|
| 666 |
+
logger.debug(
|
| 667 |
+
"Not copying link to destination directory "
|
| 668 |
+
"since it is a directory: %s",
|
| 669 |
+
link,
|
| 670 |
+
)
|
| 671 |
+
return
|
| 672 |
+
if req.local_file_path is None:
|
| 673 |
+
# No distribution was downloaded for this requirement.
|
| 674 |
+
return
|
| 675 |
+
|
| 676 |
+
download_location = os.path.join(self.download_dir, link.filename)
|
| 677 |
+
if not os.path.exists(download_location):
|
| 678 |
+
shutil.copy(req.local_file_path, download_location)
|
| 679 |
+
download_path = display_path(download_location)
|
| 680 |
+
logger.info("Saved %s", download_path)
|
| 681 |
+
|
| 682 |
+
def prepare_editable_requirement(
|
| 683 |
+
self,
|
| 684 |
+
req: InstallRequirement,
|
| 685 |
+
) -> BaseDistribution:
|
| 686 |
+
"""Prepare an editable requirement."""
|
| 687 |
+
assert req.editable, "cannot prepare a non-editable req as editable"
|
| 688 |
+
|
| 689 |
+
logger.info("Obtaining %s", req)
|
| 690 |
+
|
| 691 |
+
with indent_log():
|
| 692 |
+
if self.require_hashes:
|
| 693 |
+
raise InstallationError(
|
| 694 |
+
f"The editable requirement {req} cannot be installed when "
|
| 695 |
+
"requiring hashes, because there is no single file to "
|
| 696 |
+
"hash."
|
| 697 |
+
)
|
| 698 |
+
req.ensure_has_source_dir(self.src_dir)
|
| 699 |
+
req.update_editable()
|
| 700 |
+
assert req.source_dir
|
| 701 |
+
req.download_info = direct_url_for_editable(req.unpacked_source_directory)
|
| 702 |
+
|
| 703 |
+
dist = _get_prepared_distribution(
|
| 704 |
+
req,
|
| 705 |
+
self.build_tracker,
|
| 706 |
+
self.finder,
|
| 707 |
+
self.build_isolation,
|
| 708 |
+
self.check_build_deps,
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
req.check_if_exists(self.use_user_site)
|
| 712 |
+
|
| 713 |
+
return dist
|
| 714 |
+
|
| 715 |
+
def prepare_installed_requirement(
|
| 716 |
+
self,
|
| 717 |
+
req: InstallRequirement,
|
| 718 |
+
skip_reason: str,
|
| 719 |
+
) -> BaseDistribution:
|
| 720 |
+
"""Prepare an already-installed requirement."""
|
| 721 |
+
assert req.satisfied_by, "req should have been satisfied but isn't"
|
| 722 |
+
assert skip_reason is not None, (
|
| 723 |
+
"did not get skip reason skipped but req.satisfied_by "
|
| 724 |
+
f"is set to {req.satisfied_by}"
|
| 725 |
+
)
|
| 726 |
+
logger.info(
|
| 727 |
+
"Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version
|
| 728 |
+
)
|
| 729 |
+
with indent_log():
|
| 730 |
+
if self.require_hashes:
|
| 731 |
+
logger.debug(
|
| 732 |
+
"Since it is already installed, we are trusting this "
|
| 733 |
+
"package without checking its hash. To ensure a "
|
| 734 |
+
"completely repeatable environment, install into an "
|
| 735 |
+
"empty virtualenv."
|
| 736 |
+
)
|
| 737 |
+
return InstalledDistribution(req).get_metadata_distribution()
|
.env/lib/python3.13/site-packages/pip/_internal/pyproject.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
from collections import namedtuple
|
| 5 |
+
from typing import Any, List, Optional
|
| 6 |
+
|
| 7 |
+
if sys.version_info >= (3, 11):
|
| 8 |
+
import tomllib
|
| 9 |
+
else:
|
| 10 |
+
from pip._vendor import tomli as tomllib
|
| 11 |
+
|
| 12 |
+
from pip._vendor.packaging.requirements import InvalidRequirement
|
| 13 |
+
|
| 14 |
+
from pip._internal.exceptions import (
|
| 15 |
+
InstallationError,
|
| 16 |
+
InvalidPyProjectBuildRequires,
|
| 17 |
+
MissingPyProjectBuildRequires,
|
| 18 |
+
)
|
| 19 |
+
from pip._internal.utils.packaging import get_requirement
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _is_list_of_str(obj: Any) -> bool:
|
| 23 |
+
return isinstance(obj, list) and all(isinstance(item, str) for item in obj)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def make_pyproject_path(unpacked_source_directory: str) -> str:
|
| 27 |
+
return os.path.join(unpacked_source_directory, "pyproject.toml")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
BuildSystemDetails = namedtuple(
|
| 31 |
+
"BuildSystemDetails", ["requires", "backend", "check", "backend_path"]
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def load_pyproject_toml(
|
| 36 |
+
use_pep517: Optional[bool], pyproject_toml: str, setup_py: str, req_name: str
|
| 37 |
+
) -> Optional[BuildSystemDetails]:
|
| 38 |
+
"""Load the pyproject.toml file.
|
| 39 |
+
|
| 40 |
+
Parameters:
|
| 41 |
+
use_pep517 - Has the user requested PEP 517 processing? None
|
| 42 |
+
means the user hasn't explicitly specified.
|
| 43 |
+
pyproject_toml - Location of the project's pyproject.toml file
|
| 44 |
+
setup_py - Location of the project's setup.py file
|
| 45 |
+
req_name - The name of the requirement we're processing (for
|
| 46 |
+
error reporting)
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
None if we should use the legacy code path, otherwise a tuple
|
| 50 |
+
(
|
| 51 |
+
requirements from pyproject.toml,
|
| 52 |
+
name of PEP 517 backend,
|
| 53 |
+
requirements we should check are installed after setting
|
| 54 |
+
up the build environment
|
| 55 |
+
directory paths to import the backend from (backend-path),
|
| 56 |
+
relative to the project root.
|
| 57 |
+
)
|
| 58 |
+
"""
|
| 59 |
+
has_pyproject = os.path.isfile(pyproject_toml)
|
| 60 |
+
has_setup = os.path.isfile(setup_py)
|
| 61 |
+
|
| 62 |
+
if not has_pyproject and not has_setup:
|
| 63 |
+
raise InstallationError(
|
| 64 |
+
f"{req_name} does not appear to be a Python project: "
|
| 65 |
+
f"neither 'setup.py' nor 'pyproject.toml' found."
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if has_pyproject:
|
| 69 |
+
with open(pyproject_toml, encoding="utf-8") as f:
|
| 70 |
+
pp_toml = tomllib.loads(f.read())
|
| 71 |
+
build_system = pp_toml.get("build-system")
|
| 72 |
+
else:
|
| 73 |
+
build_system = None
|
| 74 |
+
|
| 75 |
+
# The following cases must use PEP 517
|
| 76 |
+
# We check for use_pep517 being non-None and falsy because that means
|
| 77 |
+
# the user explicitly requested --no-use-pep517. The value 0 as
|
| 78 |
+
# opposed to False can occur when the value is provided via an
|
| 79 |
+
# environment variable or config file option (due to the quirk of
|
| 80 |
+
# strtobool() returning an integer in pip's configuration code).
|
| 81 |
+
if has_pyproject and not has_setup:
|
| 82 |
+
if use_pep517 is not None and not use_pep517:
|
| 83 |
+
raise InstallationError(
|
| 84 |
+
"Disabling PEP 517 processing is invalid: "
|
| 85 |
+
"project does not have a setup.py"
|
| 86 |
+
)
|
| 87 |
+
use_pep517 = True
|
| 88 |
+
elif build_system and "build-backend" in build_system:
|
| 89 |
+
if use_pep517 is not None and not use_pep517:
|
| 90 |
+
raise InstallationError(
|
| 91 |
+
"Disabling PEP 517 processing is invalid: "
|
| 92 |
+
"project specifies a build backend of {} "
|
| 93 |
+
"in pyproject.toml".format(build_system["build-backend"])
|
| 94 |
+
)
|
| 95 |
+
use_pep517 = True
|
| 96 |
+
|
| 97 |
+
# If we haven't worked out whether to use PEP 517 yet,
|
| 98 |
+
# and the user hasn't explicitly stated a preference,
|
| 99 |
+
# we do so if the project has a pyproject.toml file
|
| 100 |
+
# or if we cannot import setuptools or wheels.
|
| 101 |
+
|
| 102 |
+
# We fallback to PEP 517 when without setuptools or without the wheel package,
|
| 103 |
+
# so setuptools can be installed as a default build backend.
|
| 104 |
+
# For more info see:
|
| 105 |
+
# https://discuss.python.org/t/pip-without-setuptools-could-the-experience-be-improved/11810/9
|
| 106 |
+
# https://github.com/pypa/pip/issues/8559
|
| 107 |
+
elif use_pep517 is None:
|
| 108 |
+
use_pep517 = (
|
| 109 |
+
has_pyproject
|
| 110 |
+
or not importlib.util.find_spec("setuptools")
|
| 111 |
+
or not importlib.util.find_spec("wheel")
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
# At this point, we know whether we're going to use PEP 517.
|
| 115 |
+
assert use_pep517 is not None
|
| 116 |
+
|
| 117 |
+
# If we're using the legacy code path, there is nothing further
|
| 118 |
+
# for us to do here.
|
| 119 |
+
if not use_pep517:
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
if build_system is None:
|
| 123 |
+
# Either the user has a pyproject.toml with no build-system
|
| 124 |
+
# section, or the user has no pyproject.toml, but has opted in
|
| 125 |
+
# explicitly via --use-pep517.
|
| 126 |
+
# In the absence of any explicit backend specification, we
|
| 127 |
+
# assume the setuptools backend that most closely emulates the
|
| 128 |
+
# traditional direct setup.py execution, and require wheel and
|
| 129 |
+
# a version of setuptools that supports that backend.
|
| 130 |
+
|
| 131 |
+
build_system = {
|
| 132 |
+
"requires": ["setuptools>=40.8.0"],
|
| 133 |
+
"build-backend": "setuptools.build_meta:__legacy__",
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
# If we're using PEP 517, we have build system information (either
|
| 137 |
+
# from pyproject.toml, or defaulted by the code above).
|
| 138 |
+
# Note that at this point, we do not know if the user has actually
|
| 139 |
+
# specified a backend, though.
|
| 140 |
+
assert build_system is not None
|
| 141 |
+
|
| 142 |
+
# Ensure that the build-system section in pyproject.toml conforms
|
| 143 |
+
# to PEP 518.
|
| 144 |
+
|
| 145 |
+
# Specifying the build-system table but not the requires key is invalid
|
| 146 |
+
if "requires" not in build_system:
|
| 147 |
+
raise MissingPyProjectBuildRequires(package=req_name)
|
| 148 |
+
|
| 149 |
+
# Error out if requires is not a list of strings
|
| 150 |
+
requires = build_system["requires"]
|
| 151 |
+
if not _is_list_of_str(requires):
|
| 152 |
+
raise InvalidPyProjectBuildRequires(
|
| 153 |
+
package=req_name,
|
| 154 |
+
reason="It is not a list of strings.",
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
# Each requirement must be valid as per PEP 508
|
| 158 |
+
for requirement in requires:
|
| 159 |
+
try:
|
| 160 |
+
get_requirement(requirement)
|
| 161 |
+
except InvalidRequirement as error:
|
| 162 |
+
raise InvalidPyProjectBuildRequires(
|
| 163 |
+
package=req_name,
|
| 164 |
+
reason=f"It contains an invalid requirement: {requirement!r}",
|
| 165 |
+
) from error
|
| 166 |
+
|
| 167 |
+
backend = build_system.get("build-backend")
|
| 168 |
+
backend_path = build_system.get("backend-path", [])
|
| 169 |
+
check: List[str] = []
|
| 170 |
+
if backend is None:
|
| 171 |
+
# If the user didn't specify a backend, we assume they want to use
|
| 172 |
+
# the setuptools backend. But we can't be sure they have included
|
| 173 |
+
# a version of setuptools which supplies the backend. So we
|
| 174 |
+
# make a note to check that this requirement is present once
|
| 175 |
+
# we have set up the environment.
|
| 176 |
+
# This is quite a lot of work to check for a very specific case. But
|
| 177 |
+
# the problem is, that case is potentially quite common - projects that
|
| 178 |
+
# adopted PEP 518 early for the ability to specify requirements to
|
| 179 |
+
# execute setup.py, but never considered needing to mention the build
|
| 180 |
+
# tools themselves. The original PEP 518 code had a similar check (but
|
| 181 |
+
# implemented in a different way).
|
| 182 |
+
backend = "setuptools.build_meta:__legacy__"
|
| 183 |
+
check = ["setuptools>=40.8.0"]
|
| 184 |
+
|
| 185 |
+
return BuildSystemDetails(requires, backend, check, backend_path)
|
.env/lib/python3.13/site-packages/pip/_internal/req/__init__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import collections
|
| 2 |
+
import logging
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Generator, List, Optional, Sequence, Tuple
|
| 5 |
+
|
| 6 |
+
from pip._internal.cli.progress_bars import get_install_progress_renderer
|
| 7 |
+
from pip._internal.utils.logging import indent_log
|
| 8 |
+
|
| 9 |
+
from .req_file import parse_requirements
|
| 10 |
+
from .req_install import InstallRequirement
|
| 11 |
+
from .req_set import RequirementSet
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"RequirementSet",
|
| 15 |
+
"InstallRequirement",
|
| 16 |
+
"parse_requirements",
|
| 17 |
+
"install_given_reqs",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass(frozen=True)
|
| 24 |
+
class InstallationResult:
|
| 25 |
+
name: str
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _validate_requirements(
|
| 29 |
+
requirements: List[InstallRequirement],
|
| 30 |
+
) -> Generator[Tuple[str, InstallRequirement], None, None]:
|
| 31 |
+
for req in requirements:
|
| 32 |
+
assert req.name, f"invalid to-be-installed requirement: {req}"
|
| 33 |
+
yield req.name, req
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def install_given_reqs(
|
| 37 |
+
requirements: List[InstallRequirement],
|
| 38 |
+
global_options: Sequence[str],
|
| 39 |
+
root: Optional[str],
|
| 40 |
+
home: Optional[str],
|
| 41 |
+
prefix: Optional[str],
|
| 42 |
+
warn_script_location: bool,
|
| 43 |
+
use_user_site: bool,
|
| 44 |
+
pycompile: bool,
|
| 45 |
+
progress_bar: str,
|
| 46 |
+
) -> List[InstallationResult]:
|
| 47 |
+
"""
|
| 48 |
+
Install everything in the given list.
|
| 49 |
+
|
| 50 |
+
(to be called after having downloaded and unpacked the packages)
|
| 51 |
+
"""
|
| 52 |
+
to_install = collections.OrderedDict(_validate_requirements(requirements))
|
| 53 |
+
|
| 54 |
+
if to_install:
|
| 55 |
+
logger.info(
|
| 56 |
+
"Installing collected packages: %s",
|
| 57 |
+
", ".join(to_install.keys()),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
installed = []
|
| 61 |
+
|
| 62 |
+
show_progress = logger.isEnabledFor(logging.INFO) and len(to_install) > 1
|
| 63 |
+
|
| 64 |
+
items = iter(to_install.values())
|
| 65 |
+
if show_progress:
|
| 66 |
+
renderer = get_install_progress_renderer(
|
| 67 |
+
bar_type=progress_bar, total=len(to_install)
|
| 68 |
+
)
|
| 69 |
+
items = renderer(items)
|
| 70 |
+
|
| 71 |
+
with indent_log():
|
| 72 |
+
for requirement in items:
|
| 73 |
+
req_name = requirement.name
|
| 74 |
+
assert req_name is not None
|
| 75 |
+
if requirement.should_reinstall:
|
| 76 |
+
logger.info("Attempting uninstall: %s", req_name)
|
| 77 |
+
with indent_log():
|
| 78 |
+
uninstalled_pathset = requirement.uninstall(auto_confirm=True)
|
| 79 |
+
else:
|
| 80 |
+
uninstalled_pathset = None
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
requirement.install(
|
| 84 |
+
global_options,
|
| 85 |
+
root=root,
|
| 86 |
+
home=home,
|
| 87 |
+
prefix=prefix,
|
| 88 |
+
warn_script_location=warn_script_location,
|
| 89 |
+
use_user_site=use_user_site,
|
| 90 |
+
pycompile=pycompile,
|
| 91 |
+
)
|
| 92 |
+
except Exception:
|
| 93 |
+
# if install did not succeed, rollback previous uninstall
|
| 94 |
+
if uninstalled_pathset and not requirement.install_succeeded:
|
| 95 |
+
uninstalled_pathset.rollback()
|
| 96 |
+
raise
|
| 97 |
+
else:
|
| 98 |
+
if uninstalled_pathset and requirement.install_succeeded:
|
| 99 |
+
uninstalled_pathset.commit()
|
| 100 |
+
|
| 101 |
+
installed.append(InstallationResult(req_name))
|
| 102 |
+
|
| 103 |
+
return installed
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (4.14 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/constructors.cpython-313.pyc
ADDED
|
Binary file (21.6 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_dependency_group.cpython-313.pyc
ADDED
|
Binary file (4.11 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_file.cpython-313.pyc
ADDED
|
Binary file (24.6 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_install.cpython-313.pyc
ADDED
|
Binary file (39.4 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_set.cpython-313.pyc
ADDED
|
Binary file (5.63 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/__pycache__/req_uninstall.cpython-313.pyc
ADDED
|
Binary file (32.9 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/req/constructors.py
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backing implementation for InstallRequirement's various constructors
|
| 2 |
+
|
| 3 |
+
The idea here is that these formed a major chunk of InstallRequirement's size
|
| 4 |
+
so, moving them and support code dedicated to them outside of that class
|
| 5 |
+
helps creates for better understandability for the rest of the code.
|
| 6 |
+
|
| 7 |
+
These are meant to be used elsewhere within pip to create instances of
|
| 8 |
+
InstallRequirement.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import copy
|
| 12 |
+
import logging
|
| 13 |
+
import os
|
| 14 |
+
import re
|
| 15 |
+
from dataclasses import dataclass
|
| 16 |
+
from typing import Collection, Dict, List, Optional, Set, Tuple, Union
|
| 17 |
+
|
| 18 |
+
from pip._vendor.packaging.markers import Marker
|
| 19 |
+
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
|
| 20 |
+
from pip._vendor.packaging.specifiers import Specifier
|
| 21 |
+
|
| 22 |
+
from pip._internal.exceptions import InstallationError
|
| 23 |
+
from pip._internal.models.index import PyPI, TestPyPI
|
| 24 |
+
from pip._internal.models.link import Link
|
| 25 |
+
from pip._internal.models.wheel import Wheel
|
| 26 |
+
from pip._internal.req.req_file import ParsedRequirement
|
| 27 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 28 |
+
from pip._internal.utils.filetypes import is_archive_file
|
| 29 |
+
from pip._internal.utils.misc import is_installable_dir
|
| 30 |
+
from pip._internal.utils.packaging import get_requirement
|
| 31 |
+
from pip._internal.utils.urls import path_to_url
|
| 32 |
+
from pip._internal.vcs import is_url, vcs
|
| 33 |
+
|
| 34 |
+
__all__ = [
|
| 35 |
+
"install_req_from_editable",
|
| 36 |
+
"install_req_from_line",
|
| 37 |
+
"parse_editable",
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
logger = logging.getLogger(__name__)
|
| 41 |
+
operators = Specifier._operators.keys()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _strip_extras(path: str) -> Tuple[str, Optional[str]]:
|
| 45 |
+
m = re.match(r"^(.+)(\[[^\]]+\])$", path)
|
| 46 |
+
extras = None
|
| 47 |
+
if m:
|
| 48 |
+
path_no_extras = m.group(1)
|
| 49 |
+
extras = m.group(2)
|
| 50 |
+
else:
|
| 51 |
+
path_no_extras = path
|
| 52 |
+
|
| 53 |
+
return path_no_extras, extras
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def convert_extras(extras: Optional[str]) -> Set[str]:
|
| 57 |
+
if not extras:
|
| 58 |
+
return set()
|
| 59 |
+
return get_requirement("placeholder" + extras.lower()).extras
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _set_requirement_extras(req: Requirement, new_extras: Set[str]) -> Requirement:
|
| 63 |
+
"""
|
| 64 |
+
Returns a new requirement based on the given one, with the supplied extras. If the
|
| 65 |
+
given requirement already has extras those are replaced (or dropped if no new extras
|
| 66 |
+
are given).
|
| 67 |
+
"""
|
| 68 |
+
match: Optional[re.Match[str]] = re.fullmatch(
|
| 69 |
+
# see https://peps.python.org/pep-0508/#complete-grammar
|
| 70 |
+
r"([\w\t .-]+)(\[[^\]]*\])?(.*)",
|
| 71 |
+
str(req),
|
| 72 |
+
flags=re.ASCII,
|
| 73 |
+
)
|
| 74 |
+
# ireq.req is a valid requirement so the regex should always match
|
| 75 |
+
assert (
|
| 76 |
+
match is not None
|
| 77 |
+
), f"regex match on requirement {req} failed, this should never happen"
|
| 78 |
+
pre: Optional[str] = match.group(1)
|
| 79 |
+
post: Optional[str] = match.group(3)
|
| 80 |
+
assert (
|
| 81 |
+
pre is not None and post is not None
|
| 82 |
+
), f"regex group selection for requirement {req} failed, this should never happen"
|
| 83 |
+
extras: str = "[{}]".format(",".join(sorted(new_extras)) if new_extras else "")
|
| 84 |
+
return get_requirement(f"{pre}{extras}{post}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def parse_editable(editable_req: str) -> Tuple[Optional[str], str, Set[str]]:
|
| 88 |
+
"""Parses an editable requirement into:
|
| 89 |
+
- a requirement name
|
| 90 |
+
- an URL
|
| 91 |
+
- extras
|
| 92 |
+
- editable options
|
| 93 |
+
Accepted requirements:
|
| 94 |
+
svn+http://blahblah@rev#egg=Foobar[baz]&subdirectory=version_subdir
|
| 95 |
+
.[some_extra]
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
url = editable_req
|
| 99 |
+
|
| 100 |
+
# If a file path is specified with extras, strip off the extras.
|
| 101 |
+
url_no_extras, extras = _strip_extras(url)
|
| 102 |
+
|
| 103 |
+
if os.path.isdir(url_no_extras):
|
| 104 |
+
# Treating it as code that has already been checked out
|
| 105 |
+
url_no_extras = path_to_url(url_no_extras)
|
| 106 |
+
|
| 107 |
+
if url_no_extras.lower().startswith("file:"):
|
| 108 |
+
package_name = Link(url_no_extras).egg_fragment
|
| 109 |
+
if extras:
|
| 110 |
+
return (
|
| 111 |
+
package_name,
|
| 112 |
+
url_no_extras,
|
| 113 |
+
get_requirement("placeholder" + extras.lower()).extras,
|
| 114 |
+
)
|
| 115 |
+
else:
|
| 116 |
+
return package_name, url_no_extras, set()
|
| 117 |
+
|
| 118 |
+
for version_control in vcs:
|
| 119 |
+
if url.lower().startswith(f"{version_control}:"):
|
| 120 |
+
url = f"{version_control}+{url}"
|
| 121 |
+
break
|
| 122 |
+
|
| 123 |
+
link = Link(url)
|
| 124 |
+
|
| 125 |
+
if not link.is_vcs:
|
| 126 |
+
backends = ", ".join(vcs.all_schemes)
|
| 127 |
+
raise InstallationError(
|
| 128 |
+
f"{editable_req} is not a valid editable requirement. "
|
| 129 |
+
f"It should either be a path to a local project or a VCS URL "
|
| 130 |
+
f"(beginning with {backends})."
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
package_name = link.egg_fragment
|
| 134 |
+
if not package_name:
|
| 135 |
+
raise InstallationError(
|
| 136 |
+
f"Could not detect requirement name for '{editable_req}', "
|
| 137 |
+
"please specify one with #egg=your_package_name"
|
| 138 |
+
)
|
| 139 |
+
return package_name, url, set()
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def check_first_requirement_in_file(filename: str) -> None:
|
| 143 |
+
"""Check if file is parsable as a requirements file.
|
| 144 |
+
|
| 145 |
+
This is heavily based on ``pkg_resources.parse_requirements``, but
|
| 146 |
+
simplified to just check the first meaningful line.
|
| 147 |
+
|
| 148 |
+
:raises InvalidRequirement: If the first meaningful line cannot be parsed
|
| 149 |
+
as an requirement.
|
| 150 |
+
"""
|
| 151 |
+
with open(filename, encoding="utf-8", errors="ignore") as f:
|
| 152 |
+
# Create a steppable iterator, so we can handle \-continuations.
|
| 153 |
+
lines = (
|
| 154 |
+
line
|
| 155 |
+
for line in (line.strip() for line in f)
|
| 156 |
+
if line and not line.startswith("#") # Skip blank lines/comments.
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
for line in lines:
|
| 160 |
+
# Drop comments -- a hash without a space may be in a URL.
|
| 161 |
+
if " #" in line:
|
| 162 |
+
line = line[: line.find(" #")]
|
| 163 |
+
# If there is a line continuation, drop it, and append the next line.
|
| 164 |
+
if line.endswith("\\"):
|
| 165 |
+
line = line[:-2].strip() + next(lines, "")
|
| 166 |
+
get_requirement(line)
|
| 167 |
+
return
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def deduce_helpful_msg(req: str) -> str:
|
| 171 |
+
"""Returns helpful msg in case requirements file does not exist,
|
| 172 |
+
or cannot be parsed.
|
| 173 |
+
|
| 174 |
+
:params req: Requirements file path
|
| 175 |
+
"""
|
| 176 |
+
if not os.path.exists(req):
|
| 177 |
+
return f" File '{req}' does not exist."
|
| 178 |
+
msg = " The path does exist. "
|
| 179 |
+
# Try to parse and check if it is a requirements file.
|
| 180 |
+
try:
|
| 181 |
+
check_first_requirement_in_file(req)
|
| 182 |
+
except InvalidRequirement:
|
| 183 |
+
logger.debug("Cannot parse '%s' as requirements file", req)
|
| 184 |
+
else:
|
| 185 |
+
msg += (
|
| 186 |
+
f"The argument you provided "
|
| 187 |
+
f"({req}) appears to be a"
|
| 188 |
+
f" requirements file. If that is the"
|
| 189 |
+
f" case, use the '-r' flag to install"
|
| 190 |
+
f" the packages specified within it."
|
| 191 |
+
)
|
| 192 |
+
return msg
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@dataclass(frozen=True)
|
| 196 |
+
class RequirementParts:
|
| 197 |
+
requirement: Optional[Requirement]
|
| 198 |
+
link: Optional[Link]
|
| 199 |
+
markers: Optional[Marker]
|
| 200 |
+
extras: Set[str]
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def parse_req_from_editable(editable_req: str) -> RequirementParts:
|
| 204 |
+
name, url, extras_override = parse_editable(editable_req)
|
| 205 |
+
|
| 206 |
+
if name is not None:
|
| 207 |
+
try:
|
| 208 |
+
req: Optional[Requirement] = get_requirement(name)
|
| 209 |
+
except InvalidRequirement as exc:
|
| 210 |
+
raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
|
| 211 |
+
else:
|
| 212 |
+
req = None
|
| 213 |
+
|
| 214 |
+
link = Link(url)
|
| 215 |
+
|
| 216 |
+
return RequirementParts(req, link, None, extras_override)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# ---- The actual constructors follow ----
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def install_req_from_editable(
|
| 223 |
+
editable_req: str,
|
| 224 |
+
comes_from: Optional[Union[InstallRequirement, str]] = None,
|
| 225 |
+
*,
|
| 226 |
+
use_pep517: Optional[bool] = None,
|
| 227 |
+
isolated: bool = False,
|
| 228 |
+
global_options: Optional[List[str]] = None,
|
| 229 |
+
hash_options: Optional[Dict[str, List[str]]] = None,
|
| 230 |
+
constraint: bool = False,
|
| 231 |
+
user_supplied: bool = False,
|
| 232 |
+
permit_editable_wheels: bool = False,
|
| 233 |
+
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
|
| 234 |
+
) -> InstallRequirement:
|
| 235 |
+
parts = parse_req_from_editable(editable_req)
|
| 236 |
+
|
| 237 |
+
return InstallRequirement(
|
| 238 |
+
parts.requirement,
|
| 239 |
+
comes_from=comes_from,
|
| 240 |
+
user_supplied=user_supplied,
|
| 241 |
+
editable=True,
|
| 242 |
+
permit_editable_wheels=permit_editable_wheels,
|
| 243 |
+
link=parts.link,
|
| 244 |
+
constraint=constraint,
|
| 245 |
+
use_pep517=use_pep517,
|
| 246 |
+
isolated=isolated,
|
| 247 |
+
global_options=global_options,
|
| 248 |
+
hash_options=hash_options,
|
| 249 |
+
config_settings=config_settings,
|
| 250 |
+
extras=parts.extras,
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def _looks_like_path(name: str) -> bool:
|
| 255 |
+
"""Checks whether the string "looks like" a path on the filesystem.
|
| 256 |
+
|
| 257 |
+
This does not check whether the target actually exists, only judge from the
|
| 258 |
+
appearance.
|
| 259 |
+
|
| 260 |
+
Returns true if any of the following conditions is true:
|
| 261 |
+
* a path separator is found (either os.path.sep or os.path.altsep);
|
| 262 |
+
* a dot is found (which represents the current directory).
|
| 263 |
+
"""
|
| 264 |
+
if os.path.sep in name:
|
| 265 |
+
return True
|
| 266 |
+
if os.path.altsep is not None and os.path.altsep in name:
|
| 267 |
+
return True
|
| 268 |
+
if name.startswith("."):
|
| 269 |
+
return True
|
| 270 |
+
return False
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def _get_url_from_path(path: str, name: str) -> Optional[str]:
|
| 274 |
+
"""
|
| 275 |
+
First, it checks whether a provided path is an installable directory. If it
|
| 276 |
+
is, returns the path.
|
| 277 |
+
|
| 278 |
+
If false, check if the path is an archive file (such as a .whl).
|
| 279 |
+
The function checks if the path is a file. If false, if the path has
|
| 280 |
+
an @, it will treat it as a PEP 440 URL requirement and return the path.
|
| 281 |
+
"""
|
| 282 |
+
if _looks_like_path(name) and os.path.isdir(path):
|
| 283 |
+
if is_installable_dir(path):
|
| 284 |
+
return path_to_url(path)
|
| 285 |
+
# TODO: The is_installable_dir test here might not be necessary
|
| 286 |
+
# now that it is done in load_pyproject_toml too.
|
| 287 |
+
raise InstallationError(
|
| 288 |
+
f"Directory {name!r} is not installable. Neither 'setup.py' "
|
| 289 |
+
"nor 'pyproject.toml' found."
|
| 290 |
+
)
|
| 291 |
+
if not is_archive_file(path):
|
| 292 |
+
return None
|
| 293 |
+
if os.path.isfile(path):
|
| 294 |
+
return path_to_url(path)
|
| 295 |
+
urlreq_parts = name.split("@", 1)
|
| 296 |
+
if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
|
| 297 |
+
# If the path contains '@' and the part before it does not look
|
| 298 |
+
# like a path, try to treat it as a PEP 440 URL req instead.
|
| 299 |
+
return None
|
| 300 |
+
logger.warning(
|
| 301 |
+
"Requirement %r looks like a filename, but the file does not exist",
|
| 302 |
+
name,
|
| 303 |
+
)
|
| 304 |
+
return path_to_url(path)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementParts:
|
| 308 |
+
if is_url(name):
|
| 309 |
+
marker_sep = "; "
|
| 310 |
+
else:
|
| 311 |
+
marker_sep = ";"
|
| 312 |
+
if marker_sep in name:
|
| 313 |
+
name, markers_as_string = name.split(marker_sep, 1)
|
| 314 |
+
markers_as_string = markers_as_string.strip()
|
| 315 |
+
if not markers_as_string:
|
| 316 |
+
markers = None
|
| 317 |
+
else:
|
| 318 |
+
markers = Marker(markers_as_string)
|
| 319 |
+
else:
|
| 320 |
+
markers = None
|
| 321 |
+
name = name.strip()
|
| 322 |
+
req_as_string = None
|
| 323 |
+
path = os.path.normpath(os.path.abspath(name))
|
| 324 |
+
link = None
|
| 325 |
+
extras_as_string = None
|
| 326 |
+
|
| 327 |
+
if is_url(name):
|
| 328 |
+
link = Link(name)
|
| 329 |
+
else:
|
| 330 |
+
p, extras_as_string = _strip_extras(path)
|
| 331 |
+
url = _get_url_from_path(p, name)
|
| 332 |
+
if url is not None:
|
| 333 |
+
link = Link(url)
|
| 334 |
+
|
| 335 |
+
# it's a local file, dir, or url
|
| 336 |
+
if link:
|
| 337 |
+
# Handle relative file URLs
|
| 338 |
+
if link.scheme == "file" and re.search(r"\.\./", link.url):
|
| 339 |
+
link = Link(path_to_url(os.path.normpath(os.path.abspath(link.path))))
|
| 340 |
+
# wheel file
|
| 341 |
+
if link.is_wheel:
|
| 342 |
+
wheel = Wheel(link.filename) # can raise InvalidWheelFilename
|
| 343 |
+
req_as_string = f"{wheel.name}=={wheel.version}"
|
| 344 |
+
else:
|
| 345 |
+
# set the req to the egg fragment. when it's not there, this
|
| 346 |
+
# will become an 'unnamed' requirement
|
| 347 |
+
req_as_string = link.egg_fragment
|
| 348 |
+
|
| 349 |
+
# a requirement specifier
|
| 350 |
+
else:
|
| 351 |
+
req_as_string = name
|
| 352 |
+
|
| 353 |
+
extras = convert_extras(extras_as_string)
|
| 354 |
+
|
| 355 |
+
def with_source(text: str) -> str:
|
| 356 |
+
if not line_source:
|
| 357 |
+
return text
|
| 358 |
+
return f"{text} (from {line_source})"
|
| 359 |
+
|
| 360 |
+
def _parse_req_string(req_as_string: str) -> Requirement:
|
| 361 |
+
try:
|
| 362 |
+
return get_requirement(req_as_string)
|
| 363 |
+
except InvalidRequirement as exc:
|
| 364 |
+
if os.path.sep in req_as_string:
|
| 365 |
+
add_msg = "It looks like a path."
|
| 366 |
+
add_msg += deduce_helpful_msg(req_as_string)
|
| 367 |
+
elif "=" in req_as_string and not any(
|
| 368 |
+
op in req_as_string for op in operators
|
| 369 |
+
):
|
| 370 |
+
add_msg = "= is not a valid operator. Did you mean == ?"
|
| 371 |
+
else:
|
| 372 |
+
add_msg = ""
|
| 373 |
+
msg = with_source(f"Invalid requirement: {req_as_string!r}: {exc}")
|
| 374 |
+
if add_msg:
|
| 375 |
+
msg += f"\nHint: {add_msg}"
|
| 376 |
+
raise InstallationError(msg)
|
| 377 |
+
|
| 378 |
+
if req_as_string is not None:
|
| 379 |
+
req: Optional[Requirement] = _parse_req_string(req_as_string)
|
| 380 |
+
else:
|
| 381 |
+
req = None
|
| 382 |
+
|
| 383 |
+
return RequirementParts(req, link, markers, extras)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def install_req_from_line(
|
| 387 |
+
name: str,
|
| 388 |
+
comes_from: Optional[Union[str, InstallRequirement]] = None,
|
| 389 |
+
*,
|
| 390 |
+
use_pep517: Optional[bool] = None,
|
| 391 |
+
isolated: bool = False,
|
| 392 |
+
global_options: Optional[List[str]] = None,
|
| 393 |
+
hash_options: Optional[Dict[str, List[str]]] = None,
|
| 394 |
+
constraint: bool = False,
|
| 395 |
+
line_source: Optional[str] = None,
|
| 396 |
+
user_supplied: bool = False,
|
| 397 |
+
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
|
| 398 |
+
) -> InstallRequirement:
|
| 399 |
+
"""Creates an InstallRequirement from a name, which might be a
|
| 400 |
+
requirement, directory containing 'setup.py', filename, or URL.
|
| 401 |
+
|
| 402 |
+
:param line_source: An optional string describing where the line is from,
|
| 403 |
+
for logging purposes in case of an error.
|
| 404 |
+
"""
|
| 405 |
+
parts = parse_req_from_line(name, line_source)
|
| 406 |
+
|
| 407 |
+
return InstallRequirement(
|
| 408 |
+
parts.requirement,
|
| 409 |
+
comes_from,
|
| 410 |
+
link=parts.link,
|
| 411 |
+
markers=parts.markers,
|
| 412 |
+
use_pep517=use_pep517,
|
| 413 |
+
isolated=isolated,
|
| 414 |
+
global_options=global_options,
|
| 415 |
+
hash_options=hash_options,
|
| 416 |
+
config_settings=config_settings,
|
| 417 |
+
constraint=constraint,
|
| 418 |
+
extras=parts.extras,
|
| 419 |
+
user_supplied=user_supplied,
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def install_req_from_req_string(
|
| 424 |
+
req_string: str,
|
| 425 |
+
comes_from: Optional[InstallRequirement] = None,
|
| 426 |
+
isolated: bool = False,
|
| 427 |
+
use_pep517: Optional[bool] = None,
|
| 428 |
+
user_supplied: bool = False,
|
| 429 |
+
) -> InstallRequirement:
|
| 430 |
+
try:
|
| 431 |
+
req = get_requirement(req_string)
|
| 432 |
+
except InvalidRequirement as exc:
|
| 433 |
+
raise InstallationError(f"Invalid requirement: {req_string!r}: {exc}")
|
| 434 |
+
|
| 435 |
+
domains_not_allowed = [
|
| 436 |
+
PyPI.file_storage_domain,
|
| 437 |
+
TestPyPI.file_storage_domain,
|
| 438 |
+
]
|
| 439 |
+
if (
|
| 440 |
+
req.url
|
| 441 |
+
and comes_from
|
| 442 |
+
and comes_from.link
|
| 443 |
+
and comes_from.link.netloc in domains_not_allowed
|
| 444 |
+
):
|
| 445 |
+
# Explicitly disallow pypi packages that depend on external urls
|
| 446 |
+
raise InstallationError(
|
| 447 |
+
"Packages installed from PyPI cannot depend on packages "
|
| 448 |
+
"which are not also hosted on PyPI.\n"
|
| 449 |
+
f"{comes_from.name} depends on {req} "
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
return InstallRequirement(
|
| 453 |
+
req,
|
| 454 |
+
comes_from,
|
| 455 |
+
isolated=isolated,
|
| 456 |
+
use_pep517=use_pep517,
|
| 457 |
+
user_supplied=user_supplied,
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def install_req_from_parsed_requirement(
|
| 462 |
+
parsed_req: ParsedRequirement,
|
| 463 |
+
isolated: bool = False,
|
| 464 |
+
use_pep517: Optional[bool] = None,
|
| 465 |
+
user_supplied: bool = False,
|
| 466 |
+
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
|
| 467 |
+
) -> InstallRequirement:
|
| 468 |
+
if parsed_req.is_editable:
|
| 469 |
+
req = install_req_from_editable(
|
| 470 |
+
parsed_req.requirement,
|
| 471 |
+
comes_from=parsed_req.comes_from,
|
| 472 |
+
use_pep517=use_pep517,
|
| 473 |
+
constraint=parsed_req.constraint,
|
| 474 |
+
isolated=isolated,
|
| 475 |
+
user_supplied=user_supplied,
|
| 476 |
+
config_settings=config_settings,
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
else:
|
| 480 |
+
req = install_req_from_line(
|
| 481 |
+
parsed_req.requirement,
|
| 482 |
+
comes_from=parsed_req.comes_from,
|
| 483 |
+
use_pep517=use_pep517,
|
| 484 |
+
isolated=isolated,
|
| 485 |
+
global_options=(
|
| 486 |
+
parsed_req.options.get("global_options", [])
|
| 487 |
+
if parsed_req.options
|
| 488 |
+
else []
|
| 489 |
+
),
|
| 490 |
+
hash_options=(
|
| 491 |
+
parsed_req.options.get("hashes", {}) if parsed_req.options else {}
|
| 492 |
+
),
|
| 493 |
+
constraint=parsed_req.constraint,
|
| 494 |
+
line_source=parsed_req.line_source,
|
| 495 |
+
user_supplied=user_supplied,
|
| 496 |
+
config_settings=config_settings,
|
| 497 |
+
)
|
| 498 |
+
return req
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def install_req_from_link_and_ireq(
|
| 502 |
+
link: Link, ireq: InstallRequirement
|
| 503 |
+
) -> InstallRequirement:
|
| 504 |
+
return InstallRequirement(
|
| 505 |
+
req=ireq.req,
|
| 506 |
+
comes_from=ireq.comes_from,
|
| 507 |
+
editable=ireq.editable,
|
| 508 |
+
link=link,
|
| 509 |
+
markers=ireq.markers,
|
| 510 |
+
use_pep517=ireq.use_pep517,
|
| 511 |
+
isolated=ireq.isolated,
|
| 512 |
+
global_options=ireq.global_options,
|
| 513 |
+
hash_options=ireq.hash_options,
|
| 514 |
+
config_settings=ireq.config_settings,
|
| 515 |
+
user_supplied=ireq.user_supplied,
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
def install_req_drop_extras(ireq: InstallRequirement) -> InstallRequirement:
|
| 520 |
+
"""
|
| 521 |
+
Creates a new InstallationRequirement using the given template but without
|
| 522 |
+
any extras. Sets the original requirement as the new one's parent
|
| 523 |
+
(comes_from).
|
| 524 |
+
"""
|
| 525 |
+
return InstallRequirement(
|
| 526 |
+
req=(
|
| 527 |
+
_set_requirement_extras(ireq.req, set()) if ireq.req is not None else None
|
| 528 |
+
),
|
| 529 |
+
comes_from=ireq,
|
| 530 |
+
editable=ireq.editable,
|
| 531 |
+
link=ireq.link,
|
| 532 |
+
markers=ireq.markers,
|
| 533 |
+
use_pep517=ireq.use_pep517,
|
| 534 |
+
isolated=ireq.isolated,
|
| 535 |
+
global_options=ireq.global_options,
|
| 536 |
+
hash_options=ireq.hash_options,
|
| 537 |
+
constraint=ireq.constraint,
|
| 538 |
+
extras=[],
|
| 539 |
+
config_settings=ireq.config_settings,
|
| 540 |
+
user_supplied=ireq.user_supplied,
|
| 541 |
+
permit_editable_wheels=ireq.permit_editable_wheels,
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def install_req_extend_extras(
|
| 546 |
+
ireq: InstallRequirement,
|
| 547 |
+
extras: Collection[str],
|
| 548 |
+
) -> InstallRequirement:
|
| 549 |
+
"""
|
| 550 |
+
Returns a copy of an installation requirement with some additional extras.
|
| 551 |
+
Makes a shallow copy of the ireq object.
|
| 552 |
+
"""
|
| 553 |
+
result = copy.copy(ireq)
|
| 554 |
+
result.extras = {*ireq.extras, *extras}
|
| 555 |
+
result.req = (
|
| 556 |
+
_set_requirement_extras(ireq.req, result.extras)
|
| 557 |
+
if ireq.req is not None
|
| 558 |
+
else None
|
| 559 |
+
)
|
| 560 |
+
return result
|
.env/lib/python3.13/site-packages/pip/_internal/req/req_dependency_group.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from typing import Any, Dict, Iterable, Iterator, List, Tuple
|
| 3 |
+
|
| 4 |
+
if sys.version_info >= (3, 11):
|
| 5 |
+
import tomllib
|
| 6 |
+
else:
|
| 7 |
+
from pip._vendor import tomli as tomllib
|
| 8 |
+
|
| 9 |
+
from pip._vendor.dependency_groups import DependencyGroupResolver
|
| 10 |
+
|
| 11 |
+
from pip._internal.exceptions import InstallationError
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def parse_dependency_groups(groups: List[Tuple[str, str]]) -> List[str]:
|
| 15 |
+
"""
|
| 16 |
+
Parse dependency groups data as provided via the CLI, in a `[path:]group` syntax.
|
| 17 |
+
|
| 18 |
+
Raises InstallationErrors if anything goes wrong.
|
| 19 |
+
"""
|
| 20 |
+
resolvers = _build_resolvers(path for (path, _) in groups)
|
| 21 |
+
return list(_resolve_all_groups(resolvers, groups))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _resolve_all_groups(
|
| 25 |
+
resolvers: Dict[str, DependencyGroupResolver], groups: List[Tuple[str, str]]
|
| 26 |
+
) -> Iterator[str]:
|
| 27 |
+
"""
|
| 28 |
+
Run all resolution, converting any error from `DependencyGroupResolver` into
|
| 29 |
+
an InstallationError.
|
| 30 |
+
"""
|
| 31 |
+
for path, groupname in groups:
|
| 32 |
+
resolver = resolvers[path]
|
| 33 |
+
try:
|
| 34 |
+
yield from (str(req) for req in resolver.resolve(groupname))
|
| 35 |
+
except (ValueError, TypeError, LookupError) as e:
|
| 36 |
+
raise InstallationError(
|
| 37 |
+
f"[dependency-groups] resolution failed for '{groupname}' "
|
| 38 |
+
f"from '{path}': {e}"
|
| 39 |
+
) from e
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _build_resolvers(paths: Iterable[str]) -> Dict[str, Any]:
|
| 43 |
+
resolvers = {}
|
| 44 |
+
for path in paths:
|
| 45 |
+
if path in resolvers:
|
| 46 |
+
continue
|
| 47 |
+
|
| 48 |
+
pyproject = _load_pyproject(path)
|
| 49 |
+
if "dependency-groups" not in pyproject:
|
| 50 |
+
raise InstallationError(
|
| 51 |
+
f"[dependency-groups] table was missing from '{path}'. "
|
| 52 |
+
"Cannot resolve '--group' option."
|
| 53 |
+
)
|
| 54 |
+
raw_dependency_groups = pyproject["dependency-groups"]
|
| 55 |
+
if not isinstance(raw_dependency_groups, dict):
|
| 56 |
+
raise InstallationError(
|
| 57 |
+
f"[dependency-groups] table was malformed in {path}. "
|
| 58 |
+
"Cannot resolve '--group' option."
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
resolvers[path] = DependencyGroupResolver(raw_dependency_groups)
|
| 62 |
+
return resolvers
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _load_pyproject(path: str) -> Dict[str, Any]:
|
| 66 |
+
"""
|
| 67 |
+
This helper loads a pyproject.toml as TOML.
|
| 68 |
+
|
| 69 |
+
It raises an InstallationError if the operation fails.
|
| 70 |
+
"""
|
| 71 |
+
try:
|
| 72 |
+
with open(path, "rb") as fp:
|
| 73 |
+
return tomllib.load(fp)
|
| 74 |
+
except FileNotFoundError:
|
| 75 |
+
raise InstallationError(f"{path} not found. Cannot resolve '--group' option.")
|
| 76 |
+
except tomllib.TOMLDecodeError as e:
|
| 77 |
+
raise InstallationError(f"Error parsing {path}: {e}") from e
|
| 78 |
+
except OSError as e:
|
| 79 |
+
raise InstallationError(f"Error reading {path}: {e}") from e
|
.env/lib/python3.13/site-packages/pip/_internal/req/req_file.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Requirements file parsing
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import codecs
|
| 6 |
+
import locale
|
| 7 |
+
import logging
|
| 8 |
+
import optparse
|
| 9 |
+
import os
|
| 10 |
+
import re
|
| 11 |
+
import shlex
|
| 12 |
+
import sys
|
| 13 |
+
import urllib.parse
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from optparse import Values
|
| 16 |
+
from typing import (
|
| 17 |
+
TYPE_CHECKING,
|
| 18 |
+
Any,
|
| 19 |
+
Callable,
|
| 20 |
+
Dict,
|
| 21 |
+
Generator,
|
| 22 |
+
Iterable,
|
| 23 |
+
List,
|
| 24 |
+
NoReturn,
|
| 25 |
+
Optional,
|
| 26 |
+
Tuple,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
from pip._internal.cli import cmdoptions
|
| 30 |
+
from pip._internal.exceptions import InstallationError, RequirementsFileParseError
|
| 31 |
+
from pip._internal.models.search_scope import SearchScope
|
| 32 |
+
|
| 33 |
+
if TYPE_CHECKING:
|
| 34 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 35 |
+
from pip._internal.network.session import PipSession
|
| 36 |
+
|
| 37 |
+
__all__ = ["parse_requirements"]
|
| 38 |
+
|
| 39 |
+
ReqFileLines = Iterable[Tuple[int, str]]
|
| 40 |
+
|
| 41 |
+
LineParser = Callable[[str], Tuple[str, Values]]
|
| 42 |
+
|
| 43 |
+
SCHEME_RE = re.compile(r"^(http|https|file):", re.I)
|
| 44 |
+
COMMENT_RE = re.compile(r"(^|\s+)#.*$")
|
| 45 |
+
|
| 46 |
+
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
|
| 47 |
+
# variable name consisting of only uppercase letters, digits or the '_'
|
| 48 |
+
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
|
| 49 |
+
# 2013 Edition.
|
| 50 |
+
ENV_VAR_RE = re.compile(r"(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})")
|
| 51 |
+
|
| 52 |
+
SUPPORTED_OPTIONS: List[Callable[..., optparse.Option]] = [
|
| 53 |
+
cmdoptions.index_url,
|
| 54 |
+
cmdoptions.extra_index_url,
|
| 55 |
+
cmdoptions.no_index,
|
| 56 |
+
cmdoptions.constraints,
|
| 57 |
+
cmdoptions.requirements,
|
| 58 |
+
cmdoptions.editable,
|
| 59 |
+
cmdoptions.find_links,
|
| 60 |
+
cmdoptions.no_binary,
|
| 61 |
+
cmdoptions.only_binary,
|
| 62 |
+
cmdoptions.prefer_binary,
|
| 63 |
+
cmdoptions.require_hashes,
|
| 64 |
+
cmdoptions.pre,
|
| 65 |
+
cmdoptions.trusted_host,
|
| 66 |
+
cmdoptions.use_new_feature,
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
# options to be passed to requirements
|
| 70 |
+
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
|
| 71 |
+
cmdoptions.global_options,
|
| 72 |
+
cmdoptions.hash,
|
| 73 |
+
cmdoptions.config_settings,
|
| 74 |
+
]
|
| 75 |
+
|
| 76 |
+
SUPPORTED_OPTIONS_EDITABLE_REQ: List[Callable[..., optparse.Option]] = [
|
| 77 |
+
cmdoptions.config_settings,
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# the 'dest' string values
|
| 82 |
+
SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ]
|
| 83 |
+
SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [
|
| 84 |
+
str(o().dest) for o in SUPPORTED_OPTIONS_EDITABLE_REQ
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
# order of BOMS is important: codecs.BOM_UTF16_LE is a prefix of codecs.BOM_UTF32_LE
|
| 88 |
+
# so data.startswith(BOM_UTF16_LE) would be true for UTF32_LE data
|
| 89 |
+
BOMS: List[Tuple[bytes, str]] = [
|
| 90 |
+
(codecs.BOM_UTF8, "utf-8"),
|
| 91 |
+
(codecs.BOM_UTF32, "utf-32"),
|
| 92 |
+
(codecs.BOM_UTF32_BE, "utf-32-be"),
|
| 93 |
+
(codecs.BOM_UTF32_LE, "utf-32-le"),
|
| 94 |
+
(codecs.BOM_UTF16, "utf-16"),
|
| 95 |
+
(codecs.BOM_UTF16_BE, "utf-16-be"),
|
| 96 |
+
(codecs.BOM_UTF16_LE, "utf-16-le"),
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
PEP263_ENCODING_RE = re.compile(rb"coding[:=]\s*([-\w.]+)")
|
| 100 |
+
DEFAULT_ENCODING = "utf-8"
|
| 101 |
+
|
| 102 |
+
logger = logging.getLogger(__name__)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
@dataclass(frozen=True)
|
| 106 |
+
class ParsedRequirement:
|
| 107 |
+
# TODO: replace this with slots=True when dropping Python 3.9 support.
|
| 108 |
+
__slots__ = (
|
| 109 |
+
"requirement",
|
| 110 |
+
"is_editable",
|
| 111 |
+
"comes_from",
|
| 112 |
+
"constraint",
|
| 113 |
+
"options",
|
| 114 |
+
"line_source",
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
requirement: str
|
| 118 |
+
is_editable: bool
|
| 119 |
+
comes_from: str
|
| 120 |
+
constraint: bool
|
| 121 |
+
options: Optional[Dict[str, Any]]
|
| 122 |
+
line_source: Optional[str]
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
@dataclass(frozen=True)
|
| 126 |
+
class ParsedLine:
|
| 127 |
+
__slots__ = ("filename", "lineno", "args", "opts", "constraint")
|
| 128 |
+
|
| 129 |
+
filename: str
|
| 130 |
+
lineno: int
|
| 131 |
+
args: str
|
| 132 |
+
opts: Values
|
| 133 |
+
constraint: bool
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def is_editable(self) -> bool:
|
| 137 |
+
return bool(self.opts.editables)
|
| 138 |
+
|
| 139 |
+
@property
|
| 140 |
+
def requirement(self) -> Optional[str]:
|
| 141 |
+
if self.args:
|
| 142 |
+
return self.args
|
| 143 |
+
elif self.is_editable:
|
| 144 |
+
# We don't support multiple -e on one line
|
| 145 |
+
return self.opts.editables[0]
|
| 146 |
+
return None
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def parse_requirements(
|
| 150 |
+
filename: str,
|
| 151 |
+
session: "PipSession",
|
| 152 |
+
finder: Optional["PackageFinder"] = None,
|
| 153 |
+
options: Optional[optparse.Values] = None,
|
| 154 |
+
constraint: bool = False,
|
| 155 |
+
) -> Generator[ParsedRequirement, None, None]:
|
| 156 |
+
"""Parse a requirements file and yield ParsedRequirement instances.
|
| 157 |
+
|
| 158 |
+
:param filename: Path or url of requirements file.
|
| 159 |
+
:param session: PipSession instance.
|
| 160 |
+
:param finder: Instance of pip.index.PackageFinder.
|
| 161 |
+
:param options: cli options.
|
| 162 |
+
:param constraint: If true, parsing a constraint file rather than
|
| 163 |
+
requirements file.
|
| 164 |
+
"""
|
| 165 |
+
line_parser = get_line_parser(finder)
|
| 166 |
+
parser = RequirementsFileParser(session, line_parser)
|
| 167 |
+
|
| 168 |
+
for parsed_line in parser.parse(filename, constraint):
|
| 169 |
+
parsed_req = handle_line(
|
| 170 |
+
parsed_line, options=options, finder=finder, session=session
|
| 171 |
+
)
|
| 172 |
+
if parsed_req is not None:
|
| 173 |
+
yield parsed_req
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def preprocess(content: str) -> ReqFileLines:
|
| 177 |
+
"""Split, filter, and join lines, and return a line iterator
|
| 178 |
+
|
| 179 |
+
:param content: the content of the requirements file
|
| 180 |
+
"""
|
| 181 |
+
lines_enum: ReqFileLines = enumerate(content.splitlines(), start=1)
|
| 182 |
+
lines_enum = join_lines(lines_enum)
|
| 183 |
+
lines_enum = ignore_comments(lines_enum)
|
| 184 |
+
lines_enum = expand_env_variables(lines_enum)
|
| 185 |
+
return lines_enum
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def handle_requirement_line(
|
| 189 |
+
line: ParsedLine,
|
| 190 |
+
options: Optional[optparse.Values] = None,
|
| 191 |
+
) -> ParsedRequirement:
|
| 192 |
+
# preserve for the nested code path
|
| 193 |
+
line_comes_from = "{} {} (line {})".format(
|
| 194 |
+
"-c" if line.constraint else "-r",
|
| 195 |
+
line.filename,
|
| 196 |
+
line.lineno,
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
assert line.requirement is not None
|
| 200 |
+
|
| 201 |
+
# get the options that apply to requirements
|
| 202 |
+
if line.is_editable:
|
| 203 |
+
supported_dest = SUPPORTED_OPTIONS_EDITABLE_REQ_DEST
|
| 204 |
+
else:
|
| 205 |
+
supported_dest = SUPPORTED_OPTIONS_REQ_DEST
|
| 206 |
+
req_options = {}
|
| 207 |
+
for dest in supported_dest:
|
| 208 |
+
if dest in line.opts.__dict__ and line.opts.__dict__[dest]:
|
| 209 |
+
req_options[dest] = line.opts.__dict__[dest]
|
| 210 |
+
|
| 211 |
+
line_source = f"line {line.lineno} of {line.filename}"
|
| 212 |
+
return ParsedRequirement(
|
| 213 |
+
requirement=line.requirement,
|
| 214 |
+
is_editable=line.is_editable,
|
| 215 |
+
comes_from=line_comes_from,
|
| 216 |
+
constraint=line.constraint,
|
| 217 |
+
options=req_options,
|
| 218 |
+
line_source=line_source,
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def handle_option_line(
|
| 223 |
+
opts: Values,
|
| 224 |
+
filename: str,
|
| 225 |
+
lineno: int,
|
| 226 |
+
finder: Optional["PackageFinder"] = None,
|
| 227 |
+
options: Optional[optparse.Values] = None,
|
| 228 |
+
session: Optional["PipSession"] = None,
|
| 229 |
+
) -> None:
|
| 230 |
+
if opts.hashes:
|
| 231 |
+
logger.warning(
|
| 232 |
+
"%s line %s has --hash but no requirement, and will be ignored.",
|
| 233 |
+
filename,
|
| 234 |
+
lineno,
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if options:
|
| 238 |
+
# percolate options upward
|
| 239 |
+
if opts.require_hashes:
|
| 240 |
+
options.require_hashes = opts.require_hashes
|
| 241 |
+
if opts.features_enabled:
|
| 242 |
+
options.features_enabled.extend(
|
| 243 |
+
f for f in opts.features_enabled if f not in options.features_enabled
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# set finder options
|
| 247 |
+
if finder:
|
| 248 |
+
find_links = finder.find_links
|
| 249 |
+
index_urls = finder.index_urls
|
| 250 |
+
no_index = finder.search_scope.no_index
|
| 251 |
+
if opts.no_index is True:
|
| 252 |
+
no_index = True
|
| 253 |
+
index_urls = []
|
| 254 |
+
if opts.index_url and not no_index:
|
| 255 |
+
index_urls = [opts.index_url]
|
| 256 |
+
if opts.extra_index_urls and not no_index:
|
| 257 |
+
index_urls.extend(opts.extra_index_urls)
|
| 258 |
+
if opts.find_links:
|
| 259 |
+
# FIXME: it would be nice to keep track of the source
|
| 260 |
+
# of the find_links: support a find-links local path
|
| 261 |
+
# relative to a requirements file.
|
| 262 |
+
value = opts.find_links[0]
|
| 263 |
+
req_dir = os.path.dirname(os.path.abspath(filename))
|
| 264 |
+
relative_to_reqs_file = os.path.join(req_dir, value)
|
| 265 |
+
if os.path.exists(relative_to_reqs_file):
|
| 266 |
+
value = relative_to_reqs_file
|
| 267 |
+
find_links.append(value)
|
| 268 |
+
|
| 269 |
+
if session:
|
| 270 |
+
# We need to update the auth urls in session
|
| 271 |
+
session.update_index_urls(index_urls)
|
| 272 |
+
|
| 273 |
+
search_scope = SearchScope(
|
| 274 |
+
find_links=find_links,
|
| 275 |
+
index_urls=index_urls,
|
| 276 |
+
no_index=no_index,
|
| 277 |
+
)
|
| 278 |
+
finder.search_scope = search_scope
|
| 279 |
+
|
| 280 |
+
if opts.pre:
|
| 281 |
+
finder.set_allow_all_prereleases()
|
| 282 |
+
|
| 283 |
+
if opts.prefer_binary:
|
| 284 |
+
finder.set_prefer_binary()
|
| 285 |
+
|
| 286 |
+
if session:
|
| 287 |
+
for host in opts.trusted_hosts or []:
|
| 288 |
+
source = f"line {lineno} of {filename}"
|
| 289 |
+
session.add_trusted_host(host, source=source)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def handle_line(
|
| 293 |
+
line: ParsedLine,
|
| 294 |
+
options: Optional[optparse.Values] = None,
|
| 295 |
+
finder: Optional["PackageFinder"] = None,
|
| 296 |
+
session: Optional["PipSession"] = None,
|
| 297 |
+
) -> Optional[ParsedRequirement]:
|
| 298 |
+
"""Handle a single parsed requirements line; This can result in
|
| 299 |
+
creating/yielding requirements, or updating the finder.
|
| 300 |
+
|
| 301 |
+
:param line: The parsed line to be processed.
|
| 302 |
+
:param options: CLI options.
|
| 303 |
+
:param finder: The finder - updated by non-requirement lines.
|
| 304 |
+
:param session: The session - updated by non-requirement lines.
|
| 305 |
+
|
| 306 |
+
Returns a ParsedRequirement object if the line is a requirement line,
|
| 307 |
+
otherwise returns None.
|
| 308 |
+
|
| 309 |
+
For lines that contain requirements, the only options that have an effect
|
| 310 |
+
are from SUPPORTED_OPTIONS_REQ, and they are scoped to the
|
| 311 |
+
requirement. Other options from SUPPORTED_OPTIONS may be present, but are
|
| 312 |
+
ignored.
|
| 313 |
+
|
| 314 |
+
For lines that do not contain requirements, the only options that have an
|
| 315 |
+
effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may
|
| 316 |
+
be present, but are ignored. These lines may contain multiple options
|
| 317 |
+
(although our docs imply only one is supported), and all our parsed and
|
| 318 |
+
affect the finder.
|
| 319 |
+
"""
|
| 320 |
+
|
| 321 |
+
if line.requirement is not None:
|
| 322 |
+
parsed_req = handle_requirement_line(line, options)
|
| 323 |
+
return parsed_req
|
| 324 |
+
else:
|
| 325 |
+
handle_option_line(
|
| 326 |
+
line.opts,
|
| 327 |
+
line.filename,
|
| 328 |
+
line.lineno,
|
| 329 |
+
finder,
|
| 330 |
+
options,
|
| 331 |
+
session,
|
| 332 |
+
)
|
| 333 |
+
return None
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
class RequirementsFileParser:
|
| 337 |
+
def __init__(
|
| 338 |
+
self,
|
| 339 |
+
session: "PipSession",
|
| 340 |
+
line_parser: LineParser,
|
| 341 |
+
) -> None:
|
| 342 |
+
self._session = session
|
| 343 |
+
self._line_parser = line_parser
|
| 344 |
+
|
| 345 |
+
def parse(
|
| 346 |
+
self, filename: str, constraint: bool
|
| 347 |
+
) -> Generator[ParsedLine, None, None]:
|
| 348 |
+
"""Parse a given file, yielding parsed lines."""
|
| 349 |
+
yield from self._parse_and_recurse(
|
| 350 |
+
filename, constraint, [{os.path.abspath(filename): None}]
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
def _parse_and_recurse(
|
| 354 |
+
self,
|
| 355 |
+
filename: str,
|
| 356 |
+
constraint: bool,
|
| 357 |
+
parsed_files_stack: List[Dict[str, Optional[str]]],
|
| 358 |
+
) -> Generator[ParsedLine, None, None]:
|
| 359 |
+
for line in self._parse_file(filename, constraint):
|
| 360 |
+
if line.requirement is None and (
|
| 361 |
+
line.opts.requirements or line.opts.constraints
|
| 362 |
+
):
|
| 363 |
+
# parse a nested requirements file
|
| 364 |
+
if line.opts.requirements:
|
| 365 |
+
req_path = line.opts.requirements[0]
|
| 366 |
+
nested_constraint = False
|
| 367 |
+
else:
|
| 368 |
+
req_path = line.opts.constraints[0]
|
| 369 |
+
nested_constraint = True
|
| 370 |
+
|
| 371 |
+
# original file is over http
|
| 372 |
+
if SCHEME_RE.search(filename):
|
| 373 |
+
# do a url join so relative paths work
|
| 374 |
+
req_path = urllib.parse.urljoin(filename, req_path)
|
| 375 |
+
# original file and nested file are paths
|
| 376 |
+
elif not SCHEME_RE.search(req_path):
|
| 377 |
+
# do a join so relative paths work
|
| 378 |
+
# and then abspath so that we can identify recursive references
|
| 379 |
+
req_path = os.path.abspath(
|
| 380 |
+
os.path.join(
|
| 381 |
+
os.path.dirname(filename),
|
| 382 |
+
req_path,
|
| 383 |
+
)
|
| 384 |
+
)
|
| 385 |
+
parsed_files = parsed_files_stack[0]
|
| 386 |
+
if req_path in parsed_files:
|
| 387 |
+
initial_file = parsed_files[req_path]
|
| 388 |
+
tail = (
|
| 389 |
+
f" and again in {initial_file}"
|
| 390 |
+
if initial_file is not None
|
| 391 |
+
else ""
|
| 392 |
+
)
|
| 393 |
+
raise RequirementsFileParseError(
|
| 394 |
+
f"{req_path} recursively references itself in {filename}{tail}"
|
| 395 |
+
)
|
| 396 |
+
# Keeping a track where was each file first included in
|
| 397 |
+
new_parsed_files = parsed_files.copy()
|
| 398 |
+
new_parsed_files[req_path] = filename
|
| 399 |
+
yield from self._parse_and_recurse(
|
| 400 |
+
req_path, nested_constraint, [new_parsed_files, *parsed_files_stack]
|
| 401 |
+
)
|
| 402 |
+
else:
|
| 403 |
+
yield line
|
| 404 |
+
|
| 405 |
+
def _parse_file(
|
| 406 |
+
self, filename: str, constraint: bool
|
| 407 |
+
) -> Generator[ParsedLine, None, None]:
|
| 408 |
+
_, content = get_file_content(filename, self._session)
|
| 409 |
+
|
| 410 |
+
lines_enum = preprocess(content)
|
| 411 |
+
|
| 412 |
+
for line_number, line in lines_enum:
|
| 413 |
+
try:
|
| 414 |
+
args_str, opts = self._line_parser(line)
|
| 415 |
+
except OptionParsingError as e:
|
| 416 |
+
# add offending line
|
| 417 |
+
msg = f"Invalid requirement: {line}\n{e.msg}"
|
| 418 |
+
raise RequirementsFileParseError(msg)
|
| 419 |
+
|
| 420 |
+
yield ParsedLine(
|
| 421 |
+
filename,
|
| 422 |
+
line_number,
|
| 423 |
+
args_str,
|
| 424 |
+
opts,
|
| 425 |
+
constraint,
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def get_line_parser(finder: Optional["PackageFinder"]) -> LineParser:
|
| 430 |
+
def parse_line(line: str) -> Tuple[str, Values]:
|
| 431 |
+
# Build new parser for each line since it accumulates appendable
|
| 432 |
+
# options.
|
| 433 |
+
parser = build_parser()
|
| 434 |
+
defaults = parser.get_default_values()
|
| 435 |
+
defaults.index_url = None
|
| 436 |
+
if finder:
|
| 437 |
+
defaults.format_control = finder.format_control
|
| 438 |
+
|
| 439 |
+
args_str, options_str = break_args_options(line)
|
| 440 |
+
|
| 441 |
+
try:
|
| 442 |
+
options = shlex.split(options_str)
|
| 443 |
+
except ValueError as e:
|
| 444 |
+
raise OptionParsingError(f"Could not split options: {options_str}") from e
|
| 445 |
+
|
| 446 |
+
opts, _ = parser.parse_args(options, defaults)
|
| 447 |
+
|
| 448 |
+
return args_str, opts
|
| 449 |
+
|
| 450 |
+
return parse_line
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def break_args_options(line: str) -> Tuple[str, str]:
|
| 454 |
+
"""Break up the line into an args and options string. We only want to shlex
|
| 455 |
+
(and then optparse) the options, not the args. args can contain markers
|
| 456 |
+
which are corrupted by shlex.
|
| 457 |
+
"""
|
| 458 |
+
tokens = line.split(" ")
|
| 459 |
+
args = []
|
| 460 |
+
options = tokens[:]
|
| 461 |
+
for token in tokens:
|
| 462 |
+
if token.startswith("-") or token.startswith("--"):
|
| 463 |
+
break
|
| 464 |
+
else:
|
| 465 |
+
args.append(token)
|
| 466 |
+
options.pop(0)
|
| 467 |
+
return " ".join(args), " ".join(options)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
class OptionParsingError(Exception):
|
| 471 |
+
def __init__(self, msg: str) -> None:
|
| 472 |
+
self.msg = msg
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def build_parser() -> optparse.OptionParser:
|
| 476 |
+
"""
|
| 477 |
+
Return a parser for parsing requirement lines
|
| 478 |
+
"""
|
| 479 |
+
parser = optparse.OptionParser(add_help_option=False)
|
| 480 |
+
|
| 481 |
+
option_factories = SUPPORTED_OPTIONS + SUPPORTED_OPTIONS_REQ
|
| 482 |
+
for option_factory in option_factories:
|
| 483 |
+
option = option_factory()
|
| 484 |
+
parser.add_option(option)
|
| 485 |
+
|
| 486 |
+
# By default optparse sys.exits on parsing errors. We want to wrap
|
| 487 |
+
# that in our own exception.
|
| 488 |
+
def parser_exit(self: Any, msg: str) -> "NoReturn":
|
| 489 |
+
raise OptionParsingError(msg)
|
| 490 |
+
|
| 491 |
+
# NOTE: mypy disallows assigning to a method
|
| 492 |
+
# https://github.com/python/mypy/issues/2427
|
| 493 |
+
parser.exit = parser_exit # type: ignore
|
| 494 |
+
|
| 495 |
+
return parser
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def join_lines(lines_enum: ReqFileLines) -> ReqFileLines:
|
| 499 |
+
"""Joins a line ending in '\' with the previous line (except when following
|
| 500 |
+
comments). The joined line takes on the index of the first line.
|
| 501 |
+
"""
|
| 502 |
+
primary_line_number = None
|
| 503 |
+
new_line: List[str] = []
|
| 504 |
+
for line_number, line in lines_enum:
|
| 505 |
+
if not line.endswith("\\") or COMMENT_RE.match(line):
|
| 506 |
+
if COMMENT_RE.match(line):
|
| 507 |
+
# this ensures comments are always matched later
|
| 508 |
+
line = " " + line
|
| 509 |
+
if new_line:
|
| 510 |
+
new_line.append(line)
|
| 511 |
+
assert primary_line_number is not None
|
| 512 |
+
yield primary_line_number, "".join(new_line)
|
| 513 |
+
new_line = []
|
| 514 |
+
else:
|
| 515 |
+
yield line_number, line
|
| 516 |
+
else:
|
| 517 |
+
if not new_line:
|
| 518 |
+
primary_line_number = line_number
|
| 519 |
+
new_line.append(line.strip("\\"))
|
| 520 |
+
|
| 521 |
+
# last line contains \
|
| 522 |
+
if new_line:
|
| 523 |
+
assert primary_line_number is not None
|
| 524 |
+
yield primary_line_number, "".join(new_line)
|
| 525 |
+
|
| 526 |
+
# TODO: handle space after '\'.
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def ignore_comments(lines_enum: ReqFileLines) -> ReqFileLines:
|
| 530 |
+
"""
|
| 531 |
+
Strips comments and filter empty lines.
|
| 532 |
+
"""
|
| 533 |
+
for line_number, line in lines_enum:
|
| 534 |
+
line = COMMENT_RE.sub("", line)
|
| 535 |
+
line = line.strip()
|
| 536 |
+
if line:
|
| 537 |
+
yield line_number, line
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def expand_env_variables(lines_enum: ReqFileLines) -> ReqFileLines:
|
| 541 |
+
"""Replace all environment variables that can be retrieved via `os.getenv`.
|
| 542 |
+
|
| 543 |
+
The only allowed format for environment variables defined in the
|
| 544 |
+
requirement file is `${MY_VARIABLE_1}` to ensure two things:
|
| 545 |
+
|
| 546 |
+
1. Strings that contain a `$` aren't accidentally (partially) expanded.
|
| 547 |
+
2. Ensure consistency across platforms for requirement files.
|
| 548 |
+
|
| 549 |
+
These points are the result of a discussion on the `github pull
|
| 550 |
+
request #3514 <https://github.com/pypa/pip/pull/3514>`_.
|
| 551 |
+
|
| 552 |
+
Valid characters in variable names follow the `POSIX standard
|
| 553 |
+
<http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
|
| 554 |
+
to uppercase letter, digits and the `_` (underscore).
|
| 555 |
+
"""
|
| 556 |
+
for line_number, line in lines_enum:
|
| 557 |
+
for env_var, var_name in ENV_VAR_RE.findall(line):
|
| 558 |
+
value = os.getenv(var_name)
|
| 559 |
+
if not value:
|
| 560 |
+
continue
|
| 561 |
+
|
| 562 |
+
line = line.replace(env_var, value)
|
| 563 |
+
|
| 564 |
+
yield line_number, line
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def get_file_content(url: str, session: "PipSession") -> Tuple[str, str]:
|
| 568 |
+
"""Gets the content of a file; it may be a filename, file: URL, or
|
| 569 |
+
http: URL. Returns (location, content). Content is unicode.
|
| 570 |
+
Respects # -*- coding: declarations on the retrieved files.
|
| 571 |
+
|
| 572 |
+
:param url: File path or url.
|
| 573 |
+
:param session: PipSession instance.
|
| 574 |
+
"""
|
| 575 |
+
scheme = urllib.parse.urlsplit(url).scheme
|
| 576 |
+
# Pip has special support for file:// URLs (LocalFSAdapter).
|
| 577 |
+
if scheme in ["http", "https", "file"]:
|
| 578 |
+
# Delay importing heavy network modules until absolutely necessary.
|
| 579 |
+
from pip._internal.network.utils import raise_for_status
|
| 580 |
+
|
| 581 |
+
resp = session.get(url)
|
| 582 |
+
raise_for_status(resp)
|
| 583 |
+
return resp.url, resp.text
|
| 584 |
+
|
| 585 |
+
# Assume this is a bare path.
|
| 586 |
+
try:
|
| 587 |
+
with open(url, "rb") as f:
|
| 588 |
+
raw_content = f.read()
|
| 589 |
+
except OSError as exc:
|
| 590 |
+
raise InstallationError(f"Could not open requirements file: {exc}")
|
| 591 |
+
|
| 592 |
+
content = _decode_req_file(raw_content, url)
|
| 593 |
+
|
| 594 |
+
return url, content
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
def _decode_req_file(data: bytes, url: str) -> str:
|
| 598 |
+
for bom, encoding in BOMS:
|
| 599 |
+
if data.startswith(bom):
|
| 600 |
+
return data[len(bom) :].decode(encoding)
|
| 601 |
+
|
| 602 |
+
for line in data.split(b"\n")[:2]:
|
| 603 |
+
if line[0:1] == b"#":
|
| 604 |
+
result = PEP263_ENCODING_RE.search(line)
|
| 605 |
+
if result is not None:
|
| 606 |
+
encoding = result.groups()[0].decode("ascii")
|
| 607 |
+
return data.decode(encoding)
|
| 608 |
+
|
| 609 |
+
try:
|
| 610 |
+
return data.decode(DEFAULT_ENCODING)
|
| 611 |
+
except UnicodeDecodeError:
|
| 612 |
+
locale_encoding = locale.getpreferredencoding(False) or sys.getdefaultencoding()
|
| 613 |
+
logging.warning(
|
| 614 |
+
"unable to decode data from %s with default encoding %s, "
|
| 615 |
+
"falling back to encoding from locale: %s. "
|
| 616 |
+
"If this is intentional you should specify the encoding with a "
|
| 617 |
+
"PEP-263 style comment, e.g. '# -*- coding: %s -*-'",
|
| 618 |
+
url,
|
| 619 |
+
DEFAULT_ENCODING,
|
| 620 |
+
locale_encoding,
|
| 621 |
+
locale_encoding,
|
| 622 |
+
)
|
| 623 |
+
return data.decode(locale_encoding)
|
.env/lib/python3.13/site-packages/pip/_internal/req/req_install.py
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import shutil
|
| 5 |
+
import sys
|
| 6 |
+
import uuid
|
| 7 |
+
import zipfile
|
| 8 |
+
from optparse import Values
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any, Collection, Dict, Iterable, List, Optional, Sequence, Union
|
| 11 |
+
|
| 12 |
+
from pip._vendor.packaging.markers import Marker
|
| 13 |
+
from pip._vendor.packaging.requirements import Requirement
|
| 14 |
+
from pip._vendor.packaging.specifiers import SpecifierSet
|
| 15 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 16 |
+
from pip._vendor.packaging.version import Version
|
| 17 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 18 |
+
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
|
| 19 |
+
|
| 20 |
+
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
|
| 21 |
+
from pip._internal.exceptions import InstallationError, PreviousBuildDirError
|
| 22 |
+
from pip._internal.locations import get_scheme
|
| 23 |
+
from pip._internal.metadata import (
|
| 24 |
+
BaseDistribution,
|
| 25 |
+
get_default_environment,
|
| 26 |
+
get_directory_distribution,
|
| 27 |
+
get_wheel_distribution,
|
| 28 |
+
)
|
| 29 |
+
from pip._internal.metadata.base import FilesystemWheel
|
| 30 |
+
from pip._internal.models.direct_url import DirectUrl
|
| 31 |
+
from pip._internal.models.link import Link
|
| 32 |
+
from pip._internal.operations.build.metadata import generate_metadata
|
| 33 |
+
from pip._internal.operations.build.metadata_editable import generate_editable_metadata
|
| 34 |
+
from pip._internal.operations.build.metadata_legacy import (
|
| 35 |
+
generate_metadata as generate_metadata_legacy,
|
| 36 |
+
)
|
| 37 |
+
from pip._internal.operations.install.editable_legacy import (
|
| 38 |
+
install_editable as install_editable_legacy,
|
| 39 |
+
)
|
| 40 |
+
from pip._internal.operations.install.wheel import install_wheel
|
| 41 |
+
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
|
| 42 |
+
from pip._internal.req.req_uninstall import UninstallPathSet
|
| 43 |
+
from pip._internal.utils.deprecation import deprecated
|
| 44 |
+
from pip._internal.utils.hashes import Hashes
|
| 45 |
+
from pip._internal.utils.misc import (
|
| 46 |
+
ConfiguredBuildBackendHookCaller,
|
| 47 |
+
ask_path_exists,
|
| 48 |
+
backup_dir,
|
| 49 |
+
display_path,
|
| 50 |
+
hide_url,
|
| 51 |
+
is_installable_dir,
|
| 52 |
+
redact_auth_from_requirement,
|
| 53 |
+
redact_auth_from_url,
|
| 54 |
+
)
|
| 55 |
+
from pip._internal.utils.packaging import get_requirement
|
| 56 |
+
from pip._internal.utils.subprocess import runner_with_spinner_message
|
| 57 |
+
from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
| 58 |
+
from pip._internal.utils.unpacking import unpack_file
|
| 59 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 60 |
+
from pip._internal.vcs import vcs
|
| 61 |
+
|
| 62 |
+
logger = logging.getLogger(__name__)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class InstallRequirement:
|
| 66 |
+
"""
|
| 67 |
+
Represents something that may be installed later on, may have information
|
| 68 |
+
about where to fetch the relevant requirement and also contains logic for
|
| 69 |
+
installing the said requirement.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(
|
| 73 |
+
self,
|
| 74 |
+
req: Optional[Requirement],
|
| 75 |
+
comes_from: Optional[Union[str, "InstallRequirement"]],
|
| 76 |
+
editable: bool = False,
|
| 77 |
+
link: Optional[Link] = None,
|
| 78 |
+
markers: Optional[Marker] = None,
|
| 79 |
+
use_pep517: Optional[bool] = None,
|
| 80 |
+
isolated: bool = False,
|
| 81 |
+
*,
|
| 82 |
+
global_options: Optional[List[str]] = None,
|
| 83 |
+
hash_options: Optional[Dict[str, List[str]]] = None,
|
| 84 |
+
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
|
| 85 |
+
constraint: bool = False,
|
| 86 |
+
extras: Collection[str] = (),
|
| 87 |
+
user_supplied: bool = False,
|
| 88 |
+
permit_editable_wheels: bool = False,
|
| 89 |
+
) -> None:
|
| 90 |
+
assert req is None or isinstance(req, Requirement), req
|
| 91 |
+
self.req = req
|
| 92 |
+
self.comes_from = comes_from
|
| 93 |
+
self.constraint = constraint
|
| 94 |
+
self.editable = editable
|
| 95 |
+
self.permit_editable_wheels = permit_editable_wheels
|
| 96 |
+
|
| 97 |
+
# source_dir is the local directory where the linked requirement is
|
| 98 |
+
# located, or unpacked. In case unpacking is needed, creating and
|
| 99 |
+
# populating source_dir is done by the RequirementPreparer. Note this
|
| 100 |
+
# is not necessarily the directory where pyproject.toml or setup.py is
|
| 101 |
+
# located - that one is obtained via unpacked_source_directory.
|
| 102 |
+
self.source_dir: Optional[str] = None
|
| 103 |
+
if self.editable:
|
| 104 |
+
assert link
|
| 105 |
+
if link.is_file:
|
| 106 |
+
self.source_dir = os.path.normpath(os.path.abspath(link.file_path))
|
| 107 |
+
|
| 108 |
+
# original_link is the direct URL that was provided by the user for the
|
| 109 |
+
# requirement, either directly or via a constraints file.
|
| 110 |
+
if link is None and req and req.url:
|
| 111 |
+
# PEP 508 URL requirement
|
| 112 |
+
link = Link(req.url)
|
| 113 |
+
self.link = self.original_link = link
|
| 114 |
+
|
| 115 |
+
# When this InstallRequirement is a wheel obtained from the cache of locally
|
| 116 |
+
# built wheels, this is the source link corresponding to the cache entry, which
|
| 117 |
+
# was used to download and build the cached wheel.
|
| 118 |
+
self.cached_wheel_source_link: Optional[Link] = None
|
| 119 |
+
|
| 120 |
+
# Information about the location of the artifact that was downloaded . This
|
| 121 |
+
# property is guaranteed to be set in resolver results.
|
| 122 |
+
self.download_info: Optional[DirectUrl] = None
|
| 123 |
+
|
| 124 |
+
# Path to any downloaded or already-existing package.
|
| 125 |
+
self.local_file_path: Optional[str] = None
|
| 126 |
+
if self.link and self.link.is_file:
|
| 127 |
+
self.local_file_path = self.link.file_path
|
| 128 |
+
|
| 129 |
+
if extras:
|
| 130 |
+
self.extras = extras
|
| 131 |
+
elif req:
|
| 132 |
+
self.extras = req.extras
|
| 133 |
+
else:
|
| 134 |
+
self.extras = set()
|
| 135 |
+
if markers is None and req:
|
| 136 |
+
markers = req.marker
|
| 137 |
+
self.markers = markers
|
| 138 |
+
|
| 139 |
+
# This holds the Distribution object if this requirement is already installed.
|
| 140 |
+
self.satisfied_by: Optional[BaseDistribution] = None
|
| 141 |
+
# Whether the installation process should try to uninstall an existing
|
| 142 |
+
# distribution before installing this requirement.
|
| 143 |
+
self.should_reinstall = False
|
| 144 |
+
# Temporary build location
|
| 145 |
+
self._temp_build_dir: Optional[TempDirectory] = None
|
| 146 |
+
# Set to True after successful installation
|
| 147 |
+
self.install_succeeded: Optional[bool] = None
|
| 148 |
+
# Supplied options
|
| 149 |
+
self.global_options = global_options if global_options else []
|
| 150 |
+
self.hash_options = hash_options if hash_options else {}
|
| 151 |
+
self.config_settings = config_settings
|
| 152 |
+
# Set to True after successful preparation of this requirement
|
| 153 |
+
self.prepared = False
|
| 154 |
+
# User supplied requirement are explicitly requested for installation
|
| 155 |
+
# by the user via CLI arguments or requirements files, as opposed to,
|
| 156 |
+
# e.g. dependencies, extras or constraints.
|
| 157 |
+
self.user_supplied = user_supplied
|
| 158 |
+
|
| 159 |
+
self.isolated = isolated
|
| 160 |
+
self.build_env: BuildEnvironment = NoOpBuildEnvironment()
|
| 161 |
+
|
| 162 |
+
# For PEP 517, the directory where we request the project metadata
|
| 163 |
+
# gets stored. We need this to pass to build_wheel, so the backend
|
| 164 |
+
# can ensure that the wheel matches the metadata (see the PEP for
|
| 165 |
+
# details).
|
| 166 |
+
self.metadata_directory: Optional[str] = None
|
| 167 |
+
|
| 168 |
+
# The static build requirements (from pyproject.toml)
|
| 169 |
+
self.pyproject_requires: Optional[List[str]] = None
|
| 170 |
+
|
| 171 |
+
# Build requirements that we will check are available
|
| 172 |
+
self.requirements_to_check: List[str] = []
|
| 173 |
+
|
| 174 |
+
# The PEP 517 backend we should use to build the project
|
| 175 |
+
self.pep517_backend: Optional[BuildBackendHookCaller] = None
|
| 176 |
+
|
| 177 |
+
# Are we using PEP 517 for this requirement?
|
| 178 |
+
# After pyproject.toml has been loaded, the only valid values are True
|
| 179 |
+
# and False. Before loading, None is valid (meaning "use the default").
|
| 180 |
+
# Setting an explicit value before loading pyproject.toml is supported,
|
| 181 |
+
# but after loading this flag should be treated as read only.
|
| 182 |
+
self.use_pep517 = use_pep517
|
| 183 |
+
|
| 184 |
+
# If config settings are provided, enforce PEP 517.
|
| 185 |
+
if self.config_settings:
|
| 186 |
+
if self.use_pep517 is False:
|
| 187 |
+
logger.warning(
|
| 188 |
+
"--no-use-pep517 ignored for %s "
|
| 189 |
+
"because --config-settings are specified.",
|
| 190 |
+
self,
|
| 191 |
+
)
|
| 192 |
+
self.use_pep517 = True
|
| 193 |
+
|
| 194 |
+
# This requirement needs more preparation before it can be built
|
| 195 |
+
self.needs_more_preparation = False
|
| 196 |
+
|
| 197 |
+
# This requirement needs to be unpacked before it can be installed.
|
| 198 |
+
self._archive_source: Optional[Path] = None
|
| 199 |
+
|
| 200 |
+
def __str__(self) -> str:
|
| 201 |
+
if self.req:
|
| 202 |
+
s = redact_auth_from_requirement(self.req)
|
| 203 |
+
if self.link:
|
| 204 |
+
s += f" from {redact_auth_from_url(self.link.url)}"
|
| 205 |
+
elif self.link:
|
| 206 |
+
s = redact_auth_from_url(self.link.url)
|
| 207 |
+
else:
|
| 208 |
+
s = "<InstallRequirement>"
|
| 209 |
+
if self.satisfied_by is not None:
|
| 210 |
+
if self.satisfied_by.location is not None:
|
| 211 |
+
location = display_path(self.satisfied_by.location)
|
| 212 |
+
else:
|
| 213 |
+
location = "<memory>"
|
| 214 |
+
s += f" in {location}"
|
| 215 |
+
if self.comes_from:
|
| 216 |
+
if isinstance(self.comes_from, str):
|
| 217 |
+
comes_from: Optional[str] = self.comes_from
|
| 218 |
+
else:
|
| 219 |
+
comes_from = self.comes_from.from_path()
|
| 220 |
+
if comes_from:
|
| 221 |
+
s += f" (from {comes_from})"
|
| 222 |
+
return s
|
| 223 |
+
|
| 224 |
+
def __repr__(self) -> str:
|
| 225 |
+
return (
|
| 226 |
+
f"<{self.__class__.__name__} object: "
|
| 227 |
+
f"{str(self)} editable={self.editable!r}>"
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
def format_debug(self) -> str:
|
| 231 |
+
"""An un-tested helper for getting state, for debugging."""
|
| 232 |
+
attributes = vars(self)
|
| 233 |
+
names = sorted(attributes)
|
| 234 |
+
|
| 235 |
+
state = (f"{attr}={attributes[attr]!r}" for attr in sorted(names))
|
| 236 |
+
return "<{name} object: {{{state}}}>".format(
|
| 237 |
+
name=self.__class__.__name__,
|
| 238 |
+
state=", ".join(state),
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Things that are valid for all kinds of requirements?
|
| 242 |
+
@property
|
| 243 |
+
def name(self) -> Optional[str]:
|
| 244 |
+
if self.req is None:
|
| 245 |
+
return None
|
| 246 |
+
return self.req.name
|
| 247 |
+
|
| 248 |
+
@functools.cached_property
|
| 249 |
+
def supports_pyproject_editable(self) -> bool:
|
| 250 |
+
if not self.use_pep517:
|
| 251 |
+
return False
|
| 252 |
+
assert self.pep517_backend
|
| 253 |
+
with self.build_env:
|
| 254 |
+
runner = runner_with_spinner_message(
|
| 255 |
+
"Checking if build backend supports build_editable"
|
| 256 |
+
)
|
| 257 |
+
with self.pep517_backend.subprocess_runner(runner):
|
| 258 |
+
return "build_editable" in self.pep517_backend._supported_features()
|
| 259 |
+
|
| 260 |
+
@property
|
| 261 |
+
def specifier(self) -> SpecifierSet:
|
| 262 |
+
assert self.req is not None
|
| 263 |
+
return self.req.specifier
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def is_direct(self) -> bool:
|
| 267 |
+
"""Whether this requirement was specified as a direct URL."""
|
| 268 |
+
return self.original_link is not None
|
| 269 |
+
|
| 270 |
+
@property
|
| 271 |
+
def is_pinned(self) -> bool:
|
| 272 |
+
"""Return whether I am pinned to an exact version.
|
| 273 |
+
|
| 274 |
+
For example, some-package==1.2 is pinned; some-package>1.2 is not.
|
| 275 |
+
"""
|
| 276 |
+
assert self.req is not None
|
| 277 |
+
specifiers = self.req.specifier
|
| 278 |
+
return len(specifiers) == 1 and next(iter(specifiers)).operator in {"==", "==="}
|
| 279 |
+
|
| 280 |
+
def match_markers(self, extras_requested: Optional[Iterable[str]] = None) -> bool:
|
| 281 |
+
if not extras_requested:
|
| 282 |
+
# Provide an extra to safely evaluate the markers
|
| 283 |
+
# without matching any extra
|
| 284 |
+
extras_requested = ("",)
|
| 285 |
+
if self.markers is not None:
|
| 286 |
+
return any(
|
| 287 |
+
self.markers.evaluate({"extra": extra}) for extra in extras_requested
|
| 288 |
+
)
|
| 289 |
+
else:
|
| 290 |
+
return True
|
| 291 |
+
|
| 292 |
+
@property
|
| 293 |
+
def has_hash_options(self) -> bool:
|
| 294 |
+
"""Return whether any known-good hashes are specified as options.
|
| 295 |
+
|
| 296 |
+
These activate --require-hashes mode; hashes specified as part of a
|
| 297 |
+
URL do not.
|
| 298 |
+
|
| 299 |
+
"""
|
| 300 |
+
return bool(self.hash_options)
|
| 301 |
+
|
| 302 |
+
def hashes(self, trust_internet: bool = True) -> Hashes:
|
| 303 |
+
"""Return a hash-comparer that considers my option- and URL-based
|
| 304 |
+
hashes to be known-good.
|
| 305 |
+
|
| 306 |
+
Hashes in URLs--ones embedded in the requirements file, not ones
|
| 307 |
+
downloaded from an index server--are almost peers with ones from
|
| 308 |
+
flags. They satisfy --require-hashes (whether it was implicitly or
|
| 309 |
+
explicitly activated) but do not activate it. md5 and sha224 are not
|
| 310 |
+
allowed in flags, which should nudge people toward good algos. We
|
| 311 |
+
always OR all hashes together, even ones from URLs.
|
| 312 |
+
|
| 313 |
+
:param trust_internet: Whether to trust URL-based (#md5=...) hashes
|
| 314 |
+
downloaded from the internet, as by populate_link()
|
| 315 |
+
|
| 316 |
+
"""
|
| 317 |
+
good_hashes = self.hash_options.copy()
|
| 318 |
+
if trust_internet:
|
| 319 |
+
link = self.link
|
| 320 |
+
elif self.is_direct and self.user_supplied:
|
| 321 |
+
link = self.original_link
|
| 322 |
+
else:
|
| 323 |
+
link = None
|
| 324 |
+
if link and link.hash:
|
| 325 |
+
assert link.hash_name is not None
|
| 326 |
+
good_hashes.setdefault(link.hash_name, []).append(link.hash)
|
| 327 |
+
return Hashes(good_hashes)
|
| 328 |
+
|
| 329 |
+
def from_path(self) -> Optional[str]:
|
| 330 |
+
"""Format a nice indicator to show where this "comes from" """
|
| 331 |
+
if self.req is None:
|
| 332 |
+
return None
|
| 333 |
+
s = str(self.req)
|
| 334 |
+
if self.comes_from:
|
| 335 |
+
comes_from: Optional[str]
|
| 336 |
+
if isinstance(self.comes_from, str):
|
| 337 |
+
comes_from = self.comes_from
|
| 338 |
+
else:
|
| 339 |
+
comes_from = self.comes_from.from_path()
|
| 340 |
+
if comes_from:
|
| 341 |
+
s += "->" + comes_from
|
| 342 |
+
return s
|
| 343 |
+
|
| 344 |
+
def ensure_build_location(
|
| 345 |
+
self, build_dir: str, autodelete: bool, parallel_builds: bool
|
| 346 |
+
) -> str:
|
| 347 |
+
assert build_dir is not None
|
| 348 |
+
if self._temp_build_dir is not None:
|
| 349 |
+
assert self._temp_build_dir.path
|
| 350 |
+
return self._temp_build_dir.path
|
| 351 |
+
if self.req is None:
|
| 352 |
+
# Some systems have /tmp as a symlink which confuses custom
|
| 353 |
+
# builds (such as numpy). Thus, we ensure that the real path
|
| 354 |
+
# is returned.
|
| 355 |
+
self._temp_build_dir = TempDirectory(
|
| 356 |
+
kind=tempdir_kinds.REQ_BUILD, globally_managed=True
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
return self._temp_build_dir.path
|
| 360 |
+
|
| 361 |
+
# This is the only remaining place where we manually determine the path
|
| 362 |
+
# for the temporary directory. It is only needed for editables where
|
| 363 |
+
# it is the value of the --src option.
|
| 364 |
+
|
| 365 |
+
# When parallel builds are enabled, add a UUID to the build directory
|
| 366 |
+
# name so multiple builds do not interfere with each other.
|
| 367 |
+
dir_name: str = canonicalize_name(self.req.name)
|
| 368 |
+
if parallel_builds:
|
| 369 |
+
dir_name = f"{dir_name}_{uuid.uuid4().hex}"
|
| 370 |
+
|
| 371 |
+
# FIXME: Is there a better place to create the build_dir? (hg and bzr
|
| 372 |
+
# need this)
|
| 373 |
+
if not os.path.exists(build_dir):
|
| 374 |
+
logger.debug("Creating directory %s", build_dir)
|
| 375 |
+
os.makedirs(build_dir)
|
| 376 |
+
actual_build_dir = os.path.join(build_dir, dir_name)
|
| 377 |
+
# `None` indicates that we respect the globally-configured deletion
|
| 378 |
+
# settings, which is what we actually want when auto-deleting.
|
| 379 |
+
delete_arg = None if autodelete else False
|
| 380 |
+
return TempDirectory(
|
| 381 |
+
path=actual_build_dir,
|
| 382 |
+
delete=delete_arg,
|
| 383 |
+
kind=tempdir_kinds.REQ_BUILD,
|
| 384 |
+
globally_managed=True,
|
| 385 |
+
).path
|
| 386 |
+
|
| 387 |
+
def _set_requirement(self) -> None:
|
| 388 |
+
"""Set requirement after generating metadata."""
|
| 389 |
+
assert self.req is None
|
| 390 |
+
assert self.metadata is not None
|
| 391 |
+
assert self.source_dir is not None
|
| 392 |
+
|
| 393 |
+
# Construct a Requirement object from the generated metadata
|
| 394 |
+
if isinstance(parse_version(self.metadata["Version"]), Version):
|
| 395 |
+
op = "=="
|
| 396 |
+
else:
|
| 397 |
+
op = "==="
|
| 398 |
+
|
| 399 |
+
self.req = get_requirement(
|
| 400 |
+
"".join(
|
| 401 |
+
[
|
| 402 |
+
self.metadata["Name"],
|
| 403 |
+
op,
|
| 404 |
+
self.metadata["Version"],
|
| 405 |
+
]
|
| 406 |
+
)
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
def warn_on_mismatching_name(self) -> None:
|
| 410 |
+
assert self.req is not None
|
| 411 |
+
metadata_name = canonicalize_name(self.metadata["Name"])
|
| 412 |
+
if canonicalize_name(self.req.name) == metadata_name:
|
| 413 |
+
# Everything is fine.
|
| 414 |
+
return
|
| 415 |
+
|
| 416 |
+
# If we're here, there's a mismatch. Log a warning about it.
|
| 417 |
+
logger.warning(
|
| 418 |
+
"Generating metadata for package %s "
|
| 419 |
+
"produced metadata for project name %s. Fix your "
|
| 420 |
+
"#egg=%s fragments.",
|
| 421 |
+
self.name,
|
| 422 |
+
metadata_name,
|
| 423 |
+
self.name,
|
| 424 |
+
)
|
| 425 |
+
self.req = get_requirement(metadata_name)
|
| 426 |
+
|
| 427 |
+
def check_if_exists(self, use_user_site: bool) -> None:
|
| 428 |
+
"""Find an installed distribution that satisfies or conflicts
|
| 429 |
+
with this requirement, and set self.satisfied_by or
|
| 430 |
+
self.should_reinstall appropriately.
|
| 431 |
+
"""
|
| 432 |
+
if self.req is None:
|
| 433 |
+
return
|
| 434 |
+
existing_dist = get_default_environment().get_distribution(self.req.name)
|
| 435 |
+
if not existing_dist:
|
| 436 |
+
return
|
| 437 |
+
|
| 438 |
+
version_compatible = self.req.specifier.contains(
|
| 439 |
+
existing_dist.version,
|
| 440 |
+
prereleases=True,
|
| 441 |
+
)
|
| 442 |
+
if not version_compatible:
|
| 443 |
+
self.satisfied_by = None
|
| 444 |
+
if use_user_site:
|
| 445 |
+
if existing_dist.in_usersite:
|
| 446 |
+
self.should_reinstall = True
|
| 447 |
+
elif running_under_virtualenv() and existing_dist.in_site_packages:
|
| 448 |
+
raise InstallationError(
|
| 449 |
+
f"Will not install to the user site because it will "
|
| 450 |
+
f"lack sys.path precedence to {existing_dist.raw_name} "
|
| 451 |
+
f"in {existing_dist.location}"
|
| 452 |
+
)
|
| 453 |
+
else:
|
| 454 |
+
self.should_reinstall = True
|
| 455 |
+
else:
|
| 456 |
+
if self.editable:
|
| 457 |
+
self.should_reinstall = True
|
| 458 |
+
# when installing editables, nothing pre-existing should ever
|
| 459 |
+
# satisfy
|
| 460 |
+
self.satisfied_by = None
|
| 461 |
+
else:
|
| 462 |
+
self.satisfied_by = existing_dist
|
| 463 |
+
|
| 464 |
+
# Things valid for wheels
|
| 465 |
+
@property
|
| 466 |
+
def is_wheel(self) -> bool:
|
| 467 |
+
if not self.link:
|
| 468 |
+
return False
|
| 469 |
+
return self.link.is_wheel
|
| 470 |
+
|
| 471 |
+
@property
|
| 472 |
+
def is_wheel_from_cache(self) -> bool:
|
| 473 |
+
# When True, it means that this InstallRequirement is a local wheel file in the
|
| 474 |
+
# cache of locally built wheels.
|
| 475 |
+
return self.cached_wheel_source_link is not None
|
| 476 |
+
|
| 477 |
+
# Things valid for sdists
|
| 478 |
+
@property
|
| 479 |
+
def unpacked_source_directory(self) -> str:
|
| 480 |
+
assert self.source_dir, f"No source dir for {self}"
|
| 481 |
+
return os.path.join(
|
| 482 |
+
self.source_dir, self.link and self.link.subdirectory_fragment or ""
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
@property
|
| 486 |
+
def setup_py_path(self) -> str:
|
| 487 |
+
assert self.source_dir, f"No source dir for {self}"
|
| 488 |
+
setup_py = os.path.join(self.unpacked_source_directory, "setup.py")
|
| 489 |
+
|
| 490 |
+
return setup_py
|
| 491 |
+
|
| 492 |
+
@property
|
| 493 |
+
def setup_cfg_path(self) -> str:
|
| 494 |
+
assert self.source_dir, f"No source dir for {self}"
|
| 495 |
+
setup_cfg = os.path.join(self.unpacked_source_directory, "setup.cfg")
|
| 496 |
+
|
| 497 |
+
return setup_cfg
|
| 498 |
+
|
| 499 |
+
@property
|
| 500 |
+
def pyproject_toml_path(self) -> str:
|
| 501 |
+
assert self.source_dir, f"No source dir for {self}"
|
| 502 |
+
return make_pyproject_path(self.unpacked_source_directory)
|
| 503 |
+
|
| 504 |
+
def load_pyproject_toml(self) -> None:
|
| 505 |
+
"""Load the pyproject.toml file.
|
| 506 |
+
|
| 507 |
+
After calling this routine, all of the attributes related to PEP 517
|
| 508 |
+
processing for this requirement have been set. In particular, the
|
| 509 |
+
use_pep517 attribute can be used to determine whether we should
|
| 510 |
+
follow the PEP 517 or legacy (setup.py) code path.
|
| 511 |
+
"""
|
| 512 |
+
pyproject_toml_data = load_pyproject_toml(
|
| 513 |
+
self.use_pep517, self.pyproject_toml_path, self.setup_py_path, str(self)
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
if pyproject_toml_data is None:
|
| 517 |
+
assert not self.config_settings
|
| 518 |
+
self.use_pep517 = False
|
| 519 |
+
return
|
| 520 |
+
|
| 521 |
+
self.use_pep517 = True
|
| 522 |
+
requires, backend, check, backend_path = pyproject_toml_data
|
| 523 |
+
self.requirements_to_check = check
|
| 524 |
+
self.pyproject_requires = requires
|
| 525 |
+
self.pep517_backend = ConfiguredBuildBackendHookCaller(
|
| 526 |
+
self,
|
| 527 |
+
self.unpacked_source_directory,
|
| 528 |
+
backend,
|
| 529 |
+
backend_path=backend_path,
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
def isolated_editable_sanity_check(self) -> None:
|
| 533 |
+
"""Check that an editable requirement if valid for use with PEP 517/518.
|
| 534 |
+
|
| 535 |
+
This verifies that an editable that has a pyproject.toml either supports PEP 660
|
| 536 |
+
or as a setup.py or a setup.cfg
|
| 537 |
+
"""
|
| 538 |
+
if (
|
| 539 |
+
self.editable
|
| 540 |
+
and self.use_pep517
|
| 541 |
+
and not self.supports_pyproject_editable
|
| 542 |
+
and not os.path.isfile(self.setup_py_path)
|
| 543 |
+
and not os.path.isfile(self.setup_cfg_path)
|
| 544 |
+
):
|
| 545 |
+
raise InstallationError(
|
| 546 |
+
f"Project {self} has a 'pyproject.toml' and its build "
|
| 547 |
+
f"backend is missing the 'build_editable' hook. Since it does not "
|
| 548 |
+
f"have a 'setup.py' nor a 'setup.cfg', "
|
| 549 |
+
f"it cannot be installed in editable mode. "
|
| 550 |
+
f"Consider using a build backend that supports PEP 660."
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
def prepare_metadata(self) -> None:
|
| 554 |
+
"""Ensure that project metadata is available.
|
| 555 |
+
|
| 556 |
+
Under PEP 517 and PEP 660, call the backend hook to prepare the metadata.
|
| 557 |
+
Under legacy processing, call setup.py egg-info.
|
| 558 |
+
"""
|
| 559 |
+
assert self.source_dir, f"No source dir for {self}"
|
| 560 |
+
details = self.name or f"from {self.link}"
|
| 561 |
+
|
| 562 |
+
if self.use_pep517:
|
| 563 |
+
assert self.pep517_backend is not None
|
| 564 |
+
if (
|
| 565 |
+
self.editable
|
| 566 |
+
and self.permit_editable_wheels
|
| 567 |
+
and self.supports_pyproject_editable
|
| 568 |
+
):
|
| 569 |
+
self.metadata_directory = generate_editable_metadata(
|
| 570 |
+
build_env=self.build_env,
|
| 571 |
+
backend=self.pep517_backend,
|
| 572 |
+
details=details,
|
| 573 |
+
)
|
| 574 |
+
else:
|
| 575 |
+
self.metadata_directory = generate_metadata(
|
| 576 |
+
build_env=self.build_env,
|
| 577 |
+
backend=self.pep517_backend,
|
| 578 |
+
details=details,
|
| 579 |
+
)
|
| 580 |
+
else:
|
| 581 |
+
self.metadata_directory = generate_metadata_legacy(
|
| 582 |
+
build_env=self.build_env,
|
| 583 |
+
setup_py_path=self.setup_py_path,
|
| 584 |
+
source_dir=self.unpacked_source_directory,
|
| 585 |
+
isolated=self.isolated,
|
| 586 |
+
details=details,
|
| 587 |
+
)
|
| 588 |
+
|
| 589 |
+
# Act on the newly generated metadata, based on the name and version.
|
| 590 |
+
if not self.name:
|
| 591 |
+
self._set_requirement()
|
| 592 |
+
else:
|
| 593 |
+
self.warn_on_mismatching_name()
|
| 594 |
+
|
| 595 |
+
self.assert_source_matches_version()
|
| 596 |
+
|
| 597 |
+
@property
|
| 598 |
+
def metadata(self) -> Any:
|
| 599 |
+
if not hasattr(self, "_metadata"):
|
| 600 |
+
self._metadata = self.get_dist().metadata
|
| 601 |
+
|
| 602 |
+
return self._metadata
|
| 603 |
+
|
| 604 |
+
def get_dist(self) -> BaseDistribution:
|
| 605 |
+
if self.metadata_directory:
|
| 606 |
+
return get_directory_distribution(self.metadata_directory)
|
| 607 |
+
elif self.local_file_path and self.is_wheel:
|
| 608 |
+
assert self.req is not None
|
| 609 |
+
return get_wheel_distribution(
|
| 610 |
+
FilesystemWheel(self.local_file_path),
|
| 611 |
+
canonicalize_name(self.req.name),
|
| 612 |
+
)
|
| 613 |
+
raise AssertionError(
|
| 614 |
+
f"InstallRequirement {self} has no metadata directory and no wheel: "
|
| 615 |
+
f"can't make a distribution."
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
def assert_source_matches_version(self) -> None:
|
| 619 |
+
assert self.source_dir, f"No source dir for {self}"
|
| 620 |
+
version = self.metadata["version"]
|
| 621 |
+
if self.req and self.req.specifier and version not in self.req.specifier:
|
| 622 |
+
logger.warning(
|
| 623 |
+
"Requested %s, but installing version %s",
|
| 624 |
+
self,
|
| 625 |
+
version,
|
| 626 |
+
)
|
| 627 |
+
else:
|
| 628 |
+
logger.debug(
|
| 629 |
+
"Source in %s has version %s, which satisfies requirement %s",
|
| 630 |
+
display_path(self.source_dir),
|
| 631 |
+
version,
|
| 632 |
+
self,
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
# For both source distributions and editables
|
| 636 |
+
def ensure_has_source_dir(
|
| 637 |
+
self,
|
| 638 |
+
parent_dir: str,
|
| 639 |
+
autodelete: bool = False,
|
| 640 |
+
parallel_builds: bool = False,
|
| 641 |
+
) -> None:
|
| 642 |
+
"""Ensure that a source_dir is set.
|
| 643 |
+
|
| 644 |
+
This will create a temporary build dir if the name of the requirement
|
| 645 |
+
isn't known yet.
|
| 646 |
+
|
| 647 |
+
:param parent_dir: The ideal pip parent_dir for the source_dir.
|
| 648 |
+
Generally src_dir for editables and build_dir for sdists.
|
| 649 |
+
:return: self.source_dir
|
| 650 |
+
"""
|
| 651 |
+
if self.source_dir is None:
|
| 652 |
+
self.source_dir = self.ensure_build_location(
|
| 653 |
+
parent_dir,
|
| 654 |
+
autodelete=autodelete,
|
| 655 |
+
parallel_builds=parallel_builds,
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
def needs_unpacked_archive(self, archive_source: Path) -> None:
|
| 659 |
+
assert self._archive_source is None
|
| 660 |
+
self._archive_source = archive_source
|
| 661 |
+
|
| 662 |
+
def ensure_pristine_source_checkout(self) -> None:
|
| 663 |
+
"""Ensure the source directory has not yet been built in."""
|
| 664 |
+
assert self.source_dir is not None
|
| 665 |
+
if self._archive_source is not None:
|
| 666 |
+
unpack_file(str(self._archive_source), self.source_dir)
|
| 667 |
+
elif is_installable_dir(self.source_dir):
|
| 668 |
+
# If a checkout exists, it's unwise to keep going.
|
| 669 |
+
# version inconsistencies are logged later, but do not fail
|
| 670 |
+
# the installation.
|
| 671 |
+
raise PreviousBuildDirError(
|
| 672 |
+
f"pip can't proceed with requirements '{self}' due to a "
|
| 673 |
+
f"pre-existing build directory ({self.source_dir}). This is likely "
|
| 674 |
+
"due to a previous installation that failed . pip is "
|
| 675 |
+
"being responsible and not assuming it can delete this. "
|
| 676 |
+
"Please delete it and try again."
|
| 677 |
+
)
|
| 678 |
+
|
| 679 |
+
# For editable installations
|
| 680 |
+
def update_editable(self) -> None:
|
| 681 |
+
if not self.link:
|
| 682 |
+
logger.debug(
|
| 683 |
+
"Cannot update repository at %s; repository location is unknown",
|
| 684 |
+
self.source_dir,
|
| 685 |
+
)
|
| 686 |
+
return
|
| 687 |
+
assert self.editable
|
| 688 |
+
assert self.source_dir
|
| 689 |
+
if self.link.scheme == "file":
|
| 690 |
+
# Static paths don't get updated
|
| 691 |
+
return
|
| 692 |
+
vcs_backend = vcs.get_backend_for_scheme(self.link.scheme)
|
| 693 |
+
# Editable requirements are validated in Requirement constructors.
|
| 694 |
+
# So here, if it's neither a path nor a valid VCS URL, it's a bug.
|
| 695 |
+
assert vcs_backend, f"Unsupported VCS URL {self.link.url}"
|
| 696 |
+
hidden_url = hide_url(self.link.url)
|
| 697 |
+
vcs_backend.obtain(self.source_dir, url=hidden_url, verbosity=0)
|
| 698 |
+
|
| 699 |
+
# Top-level Actions
|
| 700 |
+
def uninstall(
|
| 701 |
+
self, auto_confirm: bool = False, verbose: bool = False
|
| 702 |
+
) -> Optional[UninstallPathSet]:
|
| 703 |
+
"""
|
| 704 |
+
Uninstall the distribution currently satisfying this requirement.
|
| 705 |
+
|
| 706 |
+
Prompts before removing or modifying files unless
|
| 707 |
+
``auto_confirm`` is True.
|
| 708 |
+
|
| 709 |
+
Refuses to delete or modify files outside of ``sys.prefix`` -
|
| 710 |
+
thus uninstallation within a virtual environment can only
|
| 711 |
+
modify that virtual environment, even if the virtualenv is
|
| 712 |
+
linked to global site-packages.
|
| 713 |
+
|
| 714 |
+
"""
|
| 715 |
+
assert self.req
|
| 716 |
+
dist = get_default_environment().get_distribution(self.req.name)
|
| 717 |
+
if not dist:
|
| 718 |
+
logger.warning("Skipping %s as it is not installed.", self.name)
|
| 719 |
+
return None
|
| 720 |
+
logger.info("Found existing installation: %s", dist)
|
| 721 |
+
|
| 722 |
+
uninstalled_pathset = UninstallPathSet.from_dist(dist)
|
| 723 |
+
uninstalled_pathset.remove(auto_confirm, verbose)
|
| 724 |
+
return uninstalled_pathset
|
| 725 |
+
|
| 726 |
+
def _get_archive_name(self, path: str, parentdir: str, rootdir: str) -> str:
|
| 727 |
+
def _clean_zip_name(name: str, prefix: str) -> str:
|
| 728 |
+
assert name.startswith(
|
| 729 |
+
prefix + os.path.sep
|
| 730 |
+
), f"name {name!r} doesn't start with prefix {prefix!r}"
|
| 731 |
+
name = name[len(prefix) + 1 :]
|
| 732 |
+
name = name.replace(os.path.sep, "/")
|
| 733 |
+
return name
|
| 734 |
+
|
| 735 |
+
assert self.req is not None
|
| 736 |
+
path = os.path.join(parentdir, path)
|
| 737 |
+
name = _clean_zip_name(path, rootdir)
|
| 738 |
+
return self.req.name + "/" + name
|
| 739 |
+
|
| 740 |
+
def archive(self, build_dir: Optional[str]) -> None:
|
| 741 |
+
"""Saves archive to provided build_dir.
|
| 742 |
+
|
| 743 |
+
Used for saving downloaded VCS requirements as part of `pip download`.
|
| 744 |
+
"""
|
| 745 |
+
assert self.source_dir
|
| 746 |
+
if build_dir is None:
|
| 747 |
+
return
|
| 748 |
+
|
| 749 |
+
create_archive = True
|
| 750 |
+
archive_name = "{}-{}.zip".format(self.name, self.metadata["version"])
|
| 751 |
+
archive_path = os.path.join(build_dir, archive_name)
|
| 752 |
+
|
| 753 |
+
if os.path.exists(archive_path):
|
| 754 |
+
response = ask_path_exists(
|
| 755 |
+
f"The file {display_path(archive_path)} exists. (i)gnore, (w)ipe, "
|
| 756 |
+
"(b)ackup, (a)bort ",
|
| 757 |
+
("i", "w", "b", "a"),
|
| 758 |
+
)
|
| 759 |
+
if response == "i":
|
| 760 |
+
create_archive = False
|
| 761 |
+
elif response == "w":
|
| 762 |
+
logger.warning("Deleting %s", display_path(archive_path))
|
| 763 |
+
os.remove(archive_path)
|
| 764 |
+
elif response == "b":
|
| 765 |
+
dest_file = backup_dir(archive_path)
|
| 766 |
+
logger.warning(
|
| 767 |
+
"Backing up %s to %s",
|
| 768 |
+
display_path(archive_path),
|
| 769 |
+
display_path(dest_file),
|
| 770 |
+
)
|
| 771 |
+
shutil.move(archive_path, dest_file)
|
| 772 |
+
elif response == "a":
|
| 773 |
+
sys.exit(-1)
|
| 774 |
+
|
| 775 |
+
if not create_archive:
|
| 776 |
+
return
|
| 777 |
+
|
| 778 |
+
zip_output = zipfile.ZipFile(
|
| 779 |
+
archive_path,
|
| 780 |
+
"w",
|
| 781 |
+
zipfile.ZIP_DEFLATED,
|
| 782 |
+
allowZip64=True,
|
| 783 |
+
)
|
| 784 |
+
with zip_output:
|
| 785 |
+
dir = os.path.normcase(os.path.abspath(self.unpacked_source_directory))
|
| 786 |
+
for dirpath, dirnames, filenames in os.walk(dir):
|
| 787 |
+
for dirname in dirnames:
|
| 788 |
+
dir_arcname = self._get_archive_name(
|
| 789 |
+
dirname,
|
| 790 |
+
parentdir=dirpath,
|
| 791 |
+
rootdir=dir,
|
| 792 |
+
)
|
| 793 |
+
zipdir = zipfile.ZipInfo(dir_arcname + "/")
|
| 794 |
+
zipdir.external_attr = 0x1ED << 16 # 0o755
|
| 795 |
+
zip_output.writestr(zipdir, "")
|
| 796 |
+
for filename in filenames:
|
| 797 |
+
file_arcname = self._get_archive_name(
|
| 798 |
+
filename,
|
| 799 |
+
parentdir=dirpath,
|
| 800 |
+
rootdir=dir,
|
| 801 |
+
)
|
| 802 |
+
filename = os.path.join(dirpath, filename)
|
| 803 |
+
zip_output.write(filename, file_arcname)
|
| 804 |
+
|
| 805 |
+
logger.info("Saved %s", display_path(archive_path))
|
| 806 |
+
|
| 807 |
+
def install(
|
| 808 |
+
self,
|
| 809 |
+
global_options: Optional[Sequence[str]] = None,
|
| 810 |
+
root: Optional[str] = None,
|
| 811 |
+
home: Optional[str] = None,
|
| 812 |
+
prefix: Optional[str] = None,
|
| 813 |
+
warn_script_location: bool = True,
|
| 814 |
+
use_user_site: bool = False,
|
| 815 |
+
pycompile: bool = True,
|
| 816 |
+
) -> None:
|
| 817 |
+
assert self.req is not None
|
| 818 |
+
scheme = get_scheme(
|
| 819 |
+
self.req.name,
|
| 820 |
+
user=use_user_site,
|
| 821 |
+
home=home,
|
| 822 |
+
root=root,
|
| 823 |
+
isolated=self.isolated,
|
| 824 |
+
prefix=prefix,
|
| 825 |
+
)
|
| 826 |
+
|
| 827 |
+
if self.editable and not self.is_wheel:
|
| 828 |
+
deprecated(
|
| 829 |
+
reason=(
|
| 830 |
+
f"Legacy editable install of {self} (setup.py develop) "
|
| 831 |
+
"is deprecated."
|
| 832 |
+
),
|
| 833 |
+
replacement=(
|
| 834 |
+
"to add a pyproject.toml or enable --use-pep517, "
|
| 835 |
+
"and use setuptools >= 64. "
|
| 836 |
+
"If the resulting installation is not behaving as expected, "
|
| 837 |
+
"try using --config-settings editable_mode=compat. "
|
| 838 |
+
"Please consult the setuptools documentation for more information"
|
| 839 |
+
),
|
| 840 |
+
gone_in="25.3",
|
| 841 |
+
issue=11457,
|
| 842 |
+
)
|
| 843 |
+
if self.config_settings:
|
| 844 |
+
logger.warning(
|
| 845 |
+
"--config-settings ignored for legacy editable install of %s. "
|
| 846 |
+
"Consider upgrading to a version of setuptools "
|
| 847 |
+
"that supports PEP 660 (>= 64).",
|
| 848 |
+
self,
|
| 849 |
+
)
|
| 850 |
+
install_editable_legacy(
|
| 851 |
+
global_options=global_options if global_options is not None else [],
|
| 852 |
+
prefix=prefix,
|
| 853 |
+
home=home,
|
| 854 |
+
use_user_site=use_user_site,
|
| 855 |
+
name=self.req.name,
|
| 856 |
+
setup_py_path=self.setup_py_path,
|
| 857 |
+
isolated=self.isolated,
|
| 858 |
+
build_env=self.build_env,
|
| 859 |
+
unpacked_source_directory=self.unpacked_source_directory,
|
| 860 |
+
)
|
| 861 |
+
self.install_succeeded = True
|
| 862 |
+
return
|
| 863 |
+
|
| 864 |
+
assert self.is_wheel
|
| 865 |
+
assert self.local_file_path
|
| 866 |
+
|
| 867 |
+
install_wheel(
|
| 868 |
+
self.req.name,
|
| 869 |
+
self.local_file_path,
|
| 870 |
+
scheme=scheme,
|
| 871 |
+
req_description=str(self.req),
|
| 872 |
+
pycompile=pycompile,
|
| 873 |
+
warn_script_location=warn_script_location,
|
| 874 |
+
direct_url=self.download_info if self.is_direct else None,
|
| 875 |
+
requested=self.user_supplied,
|
| 876 |
+
)
|
| 877 |
+
self.install_succeeded = True
|
| 878 |
+
|
| 879 |
+
|
| 880 |
+
def check_invalid_constraint_type(req: InstallRequirement) -> str:
|
| 881 |
+
# Check for unsupported forms
|
| 882 |
+
problem = ""
|
| 883 |
+
if not req.name:
|
| 884 |
+
problem = "Unnamed requirements are not allowed as constraints"
|
| 885 |
+
elif req.editable:
|
| 886 |
+
problem = "Editable requirements are not allowed as constraints"
|
| 887 |
+
elif req.extras:
|
| 888 |
+
problem = "Constraints cannot have extras"
|
| 889 |
+
|
| 890 |
+
if problem:
|
| 891 |
+
deprecated(
|
| 892 |
+
reason=(
|
| 893 |
+
"Constraints are only allowed to take the form of a package "
|
| 894 |
+
"name and a version specifier. Other forms were originally "
|
| 895 |
+
"permitted as an accident of the implementation, but were "
|
| 896 |
+
"undocumented. The new implementation of the resolver no "
|
| 897 |
+
"longer supports these forms."
|
| 898 |
+
),
|
| 899 |
+
replacement="replacing the constraint with a requirement",
|
| 900 |
+
# No plan yet for when the new resolver becomes default
|
| 901 |
+
gone_in=None,
|
| 902 |
+
issue=8210,
|
| 903 |
+
)
|
| 904 |
+
|
| 905 |
+
return problem
|
| 906 |
+
|
| 907 |
+
|
| 908 |
+
def _has_option(options: Values, reqs: List[InstallRequirement], option: str) -> bool:
|
| 909 |
+
if getattr(options, option, None):
|
| 910 |
+
return True
|
| 911 |
+
for req in reqs:
|
| 912 |
+
if getattr(req, option, None):
|
| 913 |
+
return True
|
| 914 |
+
return False
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
def check_legacy_setup_py_options(
|
| 918 |
+
options: Values,
|
| 919 |
+
reqs: List[InstallRequirement],
|
| 920 |
+
) -> None:
|
| 921 |
+
has_build_options = _has_option(options, reqs, "build_options")
|
| 922 |
+
has_global_options = _has_option(options, reqs, "global_options")
|
| 923 |
+
if has_build_options or has_global_options:
|
| 924 |
+
deprecated(
|
| 925 |
+
reason="--build-option and --global-option are deprecated.",
|
| 926 |
+
issue=11859,
|
| 927 |
+
replacement="to use --config-settings",
|
| 928 |
+
gone_in="25.3",
|
| 929 |
+
)
|
| 930 |
+
logger.warning(
|
| 931 |
+
"Implying --no-binary=:all: due to the presence of "
|
| 932 |
+
"--build-option / --global-option. "
|
| 933 |
+
)
|
| 934 |
+
options.format_control.disallow_binaries()
|
.env/lib/python3.13/site-packages/pip/_internal/req/req_set.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from collections import OrderedDict
|
| 3 |
+
from typing import Dict, List
|
| 4 |
+
|
| 5 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 6 |
+
|
| 7 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class RequirementSet:
|
| 13 |
+
def __init__(self, check_supported_wheels: bool = True) -> None:
|
| 14 |
+
"""Create a RequirementSet."""
|
| 15 |
+
|
| 16 |
+
self.requirements: Dict[str, InstallRequirement] = OrderedDict()
|
| 17 |
+
self.check_supported_wheels = check_supported_wheels
|
| 18 |
+
|
| 19 |
+
self.unnamed_requirements: List[InstallRequirement] = []
|
| 20 |
+
|
| 21 |
+
def __str__(self) -> str:
|
| 22 |
+
requirements = sorted(
|
| 23 |
+
(req for req in self.requirements.values() if not req.comes_from),
|
| 24 |
+
key=lambda req: canonicalize_name(req.name or ""),
|
| 25 |
+
)
|
| 26 |
+
return " ".join(str(req.req) for req in requirements)
|
| 27 |
+
|
| 28 |
+
def __repr__(self) -> str:
|
| 29 |
+
requirements = sorted(
|
| 30 |
+
self.requirements.values(),
|
| 31 |
+
key=lambda req: canonicalize_name(req.name or ""),
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
format_string = "<{classname} object; {count} requirement(s): {reqs}>"
|
| 35 |
+
return format_string.format(
|
| 36 |
+
classname=self.__class__.__name__,
|
| 37 |
+
count=len(requirements),
|
| 38 |
+
reqs=", ".join(str(req.req) for req in requirements),
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
def add_unnamed_requirement(self, install_req: InstallRequirement) -> None:
|
| 42 |
+
assert not install_req.name
|
| 43 |
+
self.unnamed_requirements.append(install_req)
|
| 44 |
+
|
| 45 |
+
def add_named_requirement(self, install_req: InstallRequirement) -> None:
|
| 46 |
+
assert install_req.name
|
| 47 |
+
|
| 48 |
+
project_name = canonicalize_name(install_req.name)
|
| 49 |
+
self.requirements[project_name] = install_req
|
| 50 |
+
|
| 51 |
+
def has_requirement(self, name: str) -> bool:
|
| 52 |
+
project_name = canonicalize_name(name)
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
project_name in self.requirements
|
| 56 |
+
and not self.requirements[project_name].constraint
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
def get_requirement(self, name: str) -> InstallRequirement:
|
| 60 |
+
project_name = canonicalize_name(name)
|
| 61 |
+
|
| 62 |
+
if project_name in self.requirements:
|
| 63 |
+
return self.requirements[project_name]
|
| 64 |
+
|
| 65 |
+
raise KeyError(f"No project with the name {name!r}")
|
| 66 |
+
|
| 67 |
+
@property
|
| 68 |
+
def all_requirements(self) -> List[InstallRequirement]:
|
| 69 |
+
return self.unnamed_requirements + list(self.requirements.values())
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def requirements_to_install(self) -> List[InstallRequirement]:
|
| 73 |
+
"""Return the list of requirements that need to be installed.
|
| 74 |
+
|
| 75 |
+
TODO remove this property together with the legacy resolver, since the new
|
| 76 |
+
resolver only returns requirements that need to be installed.
|
| 77 |
+
"""
|
| 78 |
+
return [
|
| 79 |
+
install_req
|
| 80 |
+
for install_req in self.all_requirements
|
| 81 |
+
if not install_req.constraint and not install_req.satisfied_by
|
| 82 |
+
]
|
.env/lib/python3.13/site-packages/pip/_internal/req/req_uninstall.py
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
import sysconfig
|
| 5 |
+
from importlib.util import cache_from_source
|
| 6 |
+
from typing import Any, Callable, Dict, Generator, Iterable, List, Optional, Set, Tuple
|
| 7 |
+
|
| 8 |
+
from pip._internal.exceptions import LegacyDistutilsInstall, UninstallMissingRecord
|
| 9 |
+
from pip._internal.locations import get_bin_prefix, get_bin_user
|
| 10 |
+
from pip._internal.metadata import BaseDistribution
|
| 11 |
+
from pip._internal.utils.compat import WINDOWS
|
| 12 |
+
from pip._internal.utils.egg_link import egg_link_path_from_location
|
| 13 |
+
from pip._internal.utils.logging import getLogger, indent_log
|
| 14 |
+
from pip._internal.utils.misc import ask, normalize_path, renames, rmtree
|
| 15 |
+
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
|
| 16 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 17 |
+
|
| 18 |
+
logger = getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _script_names(
|
| 22 |
+
bin_dir: str, script_name: str, is_gui: bool
|
| 23 |
+
) -> Generator[str, None, None]:
|
| 24 |
+
"""Create the fully qualified name of the files created by
|
| 25 |
+
{console,gui}_scripts for the given ``dist``.
|
| 26 |
+
Returns the list of file names
|
| 27 |
+
"""
|
| 28 |
+
exe_name = os.path.join(bin_dir, script_name)
|
| 29 |
+
yield exe_name
|
| 30 |
+
if not WINDOWS:
|
| 31 |
+
return
|
| 32 |
+
yield f"{exe_name}.exe"
|
| 33 |
+
yield f"{exe_name}.exe.manifest"
|
| 34 |
+
if is_gui:
|
| 35 |
+
yield f"{exe_name}-script.pyw"
|
| 36 |
+
else:
|
| 37 |
+
yield f"{exe_name}-script.py"
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _unique(
|
| 41 |
+
fn: Callable[..., Generator[Any, None, None]],
|
| 42 |
+
) -> Callable[..., Generator[Any, None, None]]:
|
| 43 |
+
@functools.wraps(fn)
|
| 44 |
+
def unique(*args: Any, **kw: Any) -> Generator[Any, None, None]:
|
| 45 |
+
seen: Set[Any] = set()
|
| 46 |
+
for item in fn(*args, **kw):
|
| 47 |
+
if item not in seen:
|
| 48 |
+
seen.add(item)
|
| 49 |
+
yield item
|
| 50 |
+
|
| 51 |
+
return unique
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@_unique
|
| 55 |
+
def uninstallation_paths(dist: BaseDistribution) -> Generator[str, None, None]:
|
| 56 |
+
"""
|
| 57 |
+
Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
|
| 58 |
+
|
| 59 |
+
Yield paths to all the files in RECORD. For each .py file in RECORD, add
|
| 60 |
+
the .pyc and .pyo in the same directory.
|
| 61 |
+
|
| 62 |
+
UninstallPathSet.add() takes care of the __pycache__ .py[co].
|
| 63 |
+
|
| 64 |
+
If RECORD is not found, raises an error,
|
| 65 |
+
with possible information from the INSTALLER file.
|
| 66 |
+
|
| 67 |
+
https://packaging.python.org/specifications/recording-installed-packages/
|
| 68 |
+
"""
|
| 69 |
+
location = dist.location
|
| 70 |
+
assert location is not None, "not installed"
|
| 71 |
+
|
| 72 |
+
entries = dist.iter_declared_entries()
|
| 73 |
+
if entries is None:
|
| 74 |
+
raise UninstallMissingRecord(distribution=dist)
|
| 75 |
+
|
| 76 |
+
for entry in entries:
|
| 77 |
+
path = os.path.join(location, entry)
|
| 78 |
+
yield path
|
| 79 |
+
if path.endswith(".py"):
|
| 80 |
+
dn, fn = os.path.split(path)
|
| 81 |
+
base = fn[:-3]
|
| 82 |
+
path = os.path.join(dn, base + ".pyc")
|
| 83 |
+
yield path
|
| 84 |
+
path = os.path.join(dn, base + ".pyo")
|
| 85 |
+
yield path
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def compact(paths: Iterable[str]) -> Set[str]:
|
| 89 |
+
"""Compact a path set to contain the minimal number of paths
|
| 90 |
+
necessary to contain all paths in the set. If /a/path/ and
|
| 91 |
+
/a/path/to/a/file.txt are both in the set, leave only the
|
| 92 |
+
shorter path."""
|
| 93 |
+
|
| 94 |
+
sep = os.path.sep
|
| 95 |
+
short_paths: Set[str] = set()
|
| 96 |
+
for path in sorted(paths, key=len):
|
| 97 |
+
should_skip = any(
|
| 98 |
+
path.startswith(shortpath.rstrip("*"))
|
| 99 |
+
and path[len(shortpath.rstrip("*").rstrip(sep))] == sep
|
| 100 |
+
for shortpath in short_paths
|
| 101 |
+
)
|
| 102 |
+
if not should_skip:
|
| 103 |
+
short_paths.add(path)
|
| 104 |
+
return short_paths
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def compress_for_rename(paths: Iterable[str]) -> Set[str]:
|
| 108 |
+
"""Returns a set containing the paths that need to be renamed.
|
| 109 |
+
|
| 110 |
+
This set may include directories when the original sequence of paths
|
| 111 |
+
included every file on disk.
|
| 112 |
+
"""
|
| 113 |
+
case_map = {os.path.normcase(p): p for p in paths}
|
| 114 |
+
remaining = set(case_map)
|
| 115 |
+
unchecked = sorted({os.path.split(p)[0] for p in case_map.values()}, key=len)
|
| 116 |
+
wildcards: Set[str] = set()
|
| 117 |
+
|
| 118 |
+
def norm_join(*a: str) -> str:
|
| 119 |
+
return os.path.normcase(os.path.join(*a))
|
| 120 |
+
|
| 121 |
+
for root in unchecked:
|
| 122 |
+
if any(os.path.normcase(root).startswith(w) for w in wildcards):
|
| 123 |
+
# This directory has already been handled.
|
| 124 |
+
continue
|
| 125 |
+
|
| 126 |
+
all_files: Set[str] = set()
|
| 127 |
+
all_subdirs: Set[str] = set()
|
| 128 |
+
for dirname, subdirs, files in os.walk(root):
|
| 129 |
+
all_subdirs.update(norm_join(root, dirname, d) for d in subdirs)
|
| 130 |
+
all_files.update(norm_join(root, dirname, f) for f in files)
|
| 131 |
+
# If all the files we found are in our remaining set of files to
|
| 132 |
+
# remove, then remove them from the latter set and add a wildcard
|
| 133 |
+
# for the directory.
|
| 134 |
+
if not (all_files - remaining):
|
| 135 |
+
remaining.difference_update(all_files)
|
| 136 |
+
wildcards.add(root + os.sep)
|
| 137 |
+
|
| 138 |
+
return set(map(case_map.__getitem__, remaining)) | wildcards
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def compress_for_output_listing(paths: Iterable[str]) -> Tuple[Set[str], Set[str]]:
|
| 142 |
+
"""Returns a tuple of 2 sets of which paths to display to user
|
| 143 |
+
|
| 144 |
+
The first set contains paths that would be deleted. Files of a package
|
| 145 |
+
are not added and the top-level directory of the package has a '*' added
|
| 146 |
+
at the end - to signify that all it's contents are removed.
|
| 147 |
+
|
| 148 |
+
The second set contains files that would have been skipped in the above
|
| 149 |
+
folders.
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
will_remove = set(paths)
|
| 153 |
+
will_skip = set()
|
| 154 |
+
|
| 155 |
+
# Determine folders and files
|
| 156 |
+
folders = set()
|
| 157 |
+
files = set()
|
| 158 |
+
for path in will_remove:
|
| 159 |
+
if path.endswith(".pyc"):
|
| 160 |
+
continue
|
| 161 |
+
if path.endswith("__init__.py") or ".dist-info" in path:
|
| 162 |
+
folders.add(os.path.dirname(path))
|
| 163 |
+
files.add(path)
|
| 164 |
+
|
| 165 |
+
_normcased_files = set(map(os.path.normcase, files))
|
| 166 |
+
|
| 167 |
+
folders = compact(folders)
|
| 168 |
+
|
| 169 |
+
# This walks the tree using os.walk to not miss extra folders
|
| 170 |
+
# that might get added.
|
| 171 |
+
for folder in folders:
|
| 172 |
+
for dirpath, _, dirfiles in os.walk(folder):
|
| 173 |
+
for fname in dirfiles:
|
| 174 |
+
if fname.endswith(".pyc"):
|
| 175 |
+
continue
|
| 176 |
+
|
| 177 |
+
file_ = os.path.join(dirpath, fname)
|
| 178 |
+
if (
|
| 179 |
+
os.path.isfile(file_)
|
| 180 |
+
and os.path.normcase(file_) not in _normcased_files
|
| 181 |
+
):
|
| 182 |
+
# We are skipping this file. Add it to the set.
|
| 183 |
+
will_skip.add(file_)
|
| 184 |
+
|
| 185 |
+
will_remove = files | {os.path.join(folder, "*") for folder in folders}
|
| 186 |
+
|
| 187 |
+
return will_remove, will_skip
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class StashedUninstallPathSet:
|
| 191 |
+
"""A set of file rename operations to stash files while
|
| 192 |
+
tentatively uninstalling them."""
|
| 193 |
+
|
| 194 |
+
def __init__(self) -> None:
|
| 195 |
+
# Mapping from source file root to [Adjacent]TempDirectory
|
| 196 |
+
# for files under that directory.
|
| 197 |
+
self._save_dirs: Dict[str, TempDirectory] = {}
|
| 198 |
+
# (old path, new path) tuples for each move that may need
|
| 199 |
+
# to be undone.
|
| 200 |
+
self._moves: List[Tuple[str, str]] = []
|
| 201 |
+
|
| 202 |
+
def _get_directory_stash(self, path: str) -> str:
|
| 203 |
+
"""Stashes a directory.
|
| 204 |
+
|
| 205 |
+
Directories are stashed adjacent to their original location if
|
| 206 |
+
possible, or else moved/copied into the user's temp dir."""
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
save_dir: TempDirectory = AdjacentTempDirectory(path)
|
| 210 |
+
except OSError:
|
| 211 |
+
save_dir = TempDirectory(kind="uninstall")
|
| 212 |
+
self._save_dirs[os.path.normcase(path)] = save_dir
|
| 213 |
+
|
| 214 |
+
return save_dir.path
|
| 215 |
+
|
| 216 |
+
def _get_file_stash(self, path: str) -> str:
|
| 217 |
+
"""Stashes a file.
|
| 218 |
+
|
| 219 |
+
If no root has been provided, one will be created for the directory
|
| 220 |
+
in the user's temp directory."""
|
| 221 |
+
path = os.path.normcase(path)
|
| 222 |
+
head, old_head = os.path.dirname(path), None
|
| 223 |
+
save_dir = None
|
| 224 |
+
|
| 225 |
+
while head != old_head:
|
| 226 |
+
try:
|
| 227 |
+
save_dir = self._save_dirs[head]
|
| 228 |
+
break
|
| 229 |
+
except KeyError:
|
| 230 |
+
pass
|
| 231 |
+
head, old_head = os.path.dirname(head), head
|
| 232 |
+
else:
|
| 233 |
+
# Did not find any suitable root
|
| 234 |
+
head = os.path.dirname(path)
|
| 235 |
+
save_dir = TempDirectory(kind="uninstall")
|
| 236 |
+
self._save_dirs[head] = save_dir
|
| 237 |
+
|
| 238 |
+
relpath = os.path.relpath(path, head)
|
| 239 |
+
if relpath and relpath != os.path.curdir:
|
| 240 |
+
return os.path.join(save_dir.path, relpath)
|
| 241 |
+
return save_dir.path
|
| 242 |
+
|
| 243 |
+
def stash(self, path: str) -> str:
|
| 244 |
+
"""Stashes the directory or file and returns its new location.
|
| 245 |
+
Handle symlinks as files to avoid modifying the symlink targets.
|
| 246 |
+
"""
|
| 247 |
+
path_is_dir = os.path.isdir(path) and not os.path.islink(path)
|
| 248 |
+
if path_is_dir:
|
| 249 |
+
new_path = self._get_directory_stash(path)
|
| 250 |
+
else:
|
| 251 |
+
new_path = self._get_file_stash(path)
|
| 252 |
+
|
| 253 |
+
self._moves.append((path, new_path))
|
| 254 |
+
if path_is_dir and os.path.isdir(new_path):
|
| 255 |
+
# If we're moving a directory, we need to
|
| 256 |
+
# remove the destination first or else it will be
|
| 257 |
+
# moved to inside the existing directory.
|
| 258 |
+
# We just created new_path ourselves, so it will
|
| 259 |
+
# be removable.
|
| 260 |
+
os.rmdir(new_path)
|
| 261 |
+
renames(path, new_path)
|
| 262 |
+
return new_path
|
| 263 |
+
|
| 264 |
+
def commit(self) -> None:
|
| 265 |
+
"""Commits the uninstall by removing stashed files."""
|
| 266 |
+
for save_dir in self._save_dirs.values():
|
| 267 |
+
save_dir.cleanup()
|
| 268 |
+
self._moves = []
|
| 269 |
+
self._save_dirs = {}
|
| 270 |
+
|
| 271 |
+
def rollback(self) -> None:
|
| 272 |
+
"""Undoes the uninstall by moving stashed files back."""
|
| 273 |
+
for p in self._moves:
|
| 274 |
+
logger.info("Moving to %s\n from %s", *p)
|
| 275 |
+
|
| 276 |
+
for new_path, path in self._moves:
|
| 277 |
+
try:
|
| 278 |
+
logger.debug("Replacing %s from %s", new_path, path)
|
| 279 |
+
if os.path.isfile(new_path) or os.path.islink(new_path):
|
| 280 |
+
os.unlink(new_path)
|
| 281 |
+
elif os.path.isdir(new_path):
|
| 282 |
+
rmtree(new_path)
|
| 283 |
+
renames(path, new_path)
|
| 284 |
+
except OSError as ex:
|
| 285 |
+
logger.error("Failed to restore %s", new_path)
|
| 286 |
+
logger.debug("Exception: %s", ex)
|
| 287 |
+
|
| 288 |
+
self.commit()
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def can_rollback(self) -> bool:
|
| 292 |
+
return bool(self._moves)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
class UninstallPathSet:
|
| 296 |
+
"""A set of file paths to be removed in the uninstallation of a
|
| 297 |
+
requirement."""
|
| 298 |
+
|
| 299 |
+
def __init__(self, dist: BaseDistribution) -> None:
|
| 300 |
+
self._paths: Set[str] = set()
|
| 301 |
+
self._refuse: Set[str] = set()
|
| 302 |
+
self._pth: Dict[str, UninstallPthEntries] = {}
|
| 303 |
+
self._dist = dist
|
| 304 |
+
self._moved_paths = StashedUninstallPathSet()
|
| 305 |
+
# Create local cache of normalize_path results. Creating an UninstallPathSet
|
| 306 |
+
# can result in hundreds/thousands of redundant calls to normalize_path with
|
| 307 |
+
# the same args, which hurts performance.
|
| 308 |
+
self._normalize_path_cached = functools.lru_cache(normalize_path)
|
| 309 |
+
|
| 310 |
+
def _permitted(self, path: str) -> bool:
|
| 311 |
+
"""
|
| 312 |
+
Return True if the given path is one we are permitted to
|
| 313 |
+
remove/modify, False otherwise.
|
| 314 |
+
|
| 315 |
+
"""
|
| 316 |
+
# aka is_local, but caching normalized sys.prefix
|
| 317 |
+
if not running_under_virtualenv():
|
| 318 |
+
return True
|
| 319 |
+
return path.startswith(self._normalize_path_cached(sys.prefix))
|
| 320 |
+
|
| 321 |
+
def add(self, path: str) -> None:
|
| 322 |
+
head, tail = os.path.split(path)
|
| 323 |
+
|
| 324 |
+
# we normalize the head to resolve parent directory symlinks, but not
|
| 325 |
+
# the tail, since we only want to uninstall symlinks, not their targets
|
| 326 |
+
path = os.path.join(self._normalize_path_cached(head), os.path.normcase(tail))
|
| 327 |
+
|
| 328 |
+
if not os.path.exists(path):
|
| 329 |
+
return
|
| 330 |
+
if self._permitted(path):
|
| 331 |
+
self._paths.add(path)
|
| 332 |
+
else:
|
| 333 |
+
self._refuse.add(path)
|
| 334 |
+
|
| 335 |
+
# __pycache__ files can show up after 'installed-files.txt' is created,
|
| 336 |
+
# due to imports
|
| 337 |
+
if os.path.splitext(path)[1] == ".py":
|
| 338 |
+
self.add(cache_from_source(path))
|
| 339 |
+
|
| 340 |
+
def add_pth(self, pth_file: str, entry: str) -> None:
|
| 341 |
+
pth_file = self._normalize_path_cached(pth_file)
|
| 342 |
+
if self._permitted(pth_file):
|
| 343 |
+
if pth_file not in self._pth:
|
| 344 |
+
self._pth[pth_file] = UninstallPthEntries(pth_file)
|
| 345 |
+
self._pth[pth_file].add(entry)
|
| 346 |
+
else:
|
| 347 |
+
self._refuse.add(pth_file)
|
| 348 |
+
|
| 349 |
+
def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None:
|
| 350 |
+
"""Remove paths in ``self._paths`` with confirmation (unless
|
| 351 |
+
``auto_confirm`` is True)."""
|
| 352 |
+
|
| 353 |
+
if not self._paths:
|
| 354 |
+
logger.info(
|
| 355 |
+
"Can't uninstall '%s'. No files were found to uninstall.",
|
| 356 |
+
self._dist.raw_name,
|
| 357 |
+
)
|
| 358 |
+
return
|
| 359 |
+
|
| 360 |
+
dist_name_version = f"{self._dist.raw_name}-{self._dist.raw_version}"
|
| 361 |
+
logger.info("Uninstalling %s:", dist_name_version)
|
| 362 |
+
|
| 363 |
+
with indent_log():
|
| 364 |
+
if auto_confirm or self._allowed_to_proceed(verbose):
|
| 365 |
+
moved = self._moved_paths
|
| 366 |
+
|
| 367 |
+
for_rename = compress_for_rename(self._paths)
|
| 368 |
+
|
| 369 |
+
for path in sorted(compact(for_rename)):
|
| 370 |
+
moved.stash(path)
|
| 371 |
+
logger.verbose("Removing file or directory %s", path)
|
| 372 |
+
|
| 373 |
+
for pth in self._pth.values():
|
| 374 |
+
pth.remove()
|
| 375 |
+
|
| 376 |
+
logger.info("Successfully uninstalled %s", dist_name_version)
|
| 377 |
+
|
| 378 |
+
def _allowed_to_proceed(self, verbose: bool) -> bool:
|
| 379 |
+
"""Display which files would be deleted and prompt for confirmation"""
|
| 380 |
+
|
| 381 |
+
def _display(msg: str, paths: Iterable[str]) -> None:
|
| 382 |
+
if not paths:
|
| 383 |
+
return
|
| 384 |
+
|
| 385 |
+
logger.info(msg)
|
| 386 |
+
with indent_log():
|
| 387 |
+
for path in sorted(compact(paths)):
|
| 388 |
+
logger.info(path)
|
| 389 |
+
|
| 390 |
+
if not verbose:
|
| 391 |
+
will_remove, will_skip = compress_for_output_listing(self._paths)
|
| 392 |
+
else:
|
| 393 |
+
# In verbose mode, display all the files that are going to be
|
| 394 |
+
# deleted.
|
| 395 |
+
will_remove = set(self._paths)
|
| 396 |
+
will_skip = set()
|
| 397 |
+
|
| 398 |
+
_display("Would remove:", will_remove)
|
| 399 |
+
_display("Would not remove (might be manually added):", will_skip)
|
| 400 |
+
_display("Would not remove (outside of prefix):", self._refuse)
|
| 401 |
+
if verbose:
|
| 402 |
+
_display("Will actually move:", compress_for_rename(self._paths))
|
| 403 |
+
|
| 404 |
+
return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
|
| 405 |
+
|
| 406 |
+
def rollback(self) -> None:
|
| 407 |
+
"""Rollback the changes previously made by remove()."""
|
| 408 |
+
if not self._moved_paths.can_rollback:
|
| 409 |
+
logger.error(
|
| 410 |
+
"Can't roll back %s; was not uninstalled",
|
| 411 |
+
self._dist.raw_name,
|
| 412 |
+
)
|
| 413 |
+
return
|
| 414 |
+
logger.info("Rolling back uninstall of %s", self._dist.raw_name)
|
| 415 |
+
self._moved_paths.rollback()
|
| 416 |
+
for pth in self._pth.values():
|
| 417 |
+
pth.rollback()
|
| 418 |
+
|
| 419 |
+
def commit(self) -> None:
|
| 420 |
+
"""Remove temporary save dir: rollback will no longer be possible."""
|
| 421 |
+
self._moved_paths.commit()
|
| 422 |
+
|
| 423 |
+
@classmethod
|
| 424 |
+
def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
|
| 425 |
+
dist_location = dist.location
|
| 426 |
+
info_location = dist.info_location
|
| 427 |
+
if dist_location is None:
|
| 428 |
+
logger.info(
|
| 429 |
+
"Not uninstalling %s since it is not installed",
|
| 430 |
+
dist.canonical_name,
|
| 431 |
+
)
|
| 432 |
+
return cls(dist)
|
| 433 |
+
|
| 434 |
+
normalized_dist_location = normalize_path(dist_location)
|
| 435 |
+
if not dist.local:
|
| 436 |
+
logger.info(
|
| 437 |
+
"Not uninstalling %s at %s, outside environment %s",
|
| 438 |
+
dist.canonical_name,
|
| 439 |
+
normalized_dist_location,
|
| 440 |
+
sys.prefix,
|
| 441 |
+
)
|
| 442 |
+
return cls(dist)
|
| 443 |
+
|
| 444 |
+
if normalized_dist_location in {
|
| 445 |
+
p
|
| 446 |
+
for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
|
| 447 |
+
if p
|
| 448 |
+
}:
|
| 449 |
+
logger.info(
|
| 450 |
+
"Not uninstalling %s at %s, as it is in the standard library.",
|
| 451 |
+
dist.canonical_name,
|
| 452 |
+
normalized_dist_location,
|
| 453 |
+
)
|
| 454 |
+
return cls(dist)
|
| 455 |
+
|
| 456 |
+
paths_to_remove = cls(dist)
|
| 457 |
+
develop_egg_link = egg_link_path_from_location(dist.raw_name)
|
| 458 |
+
|
| 459 |
+
# Distribution is installed with metadata in a "flat" .egg-info
|
| 460 |
+
# directory. This means it is not a modern .dist-info installation, an
|
| 461 |
+
# egg, or legacy editable.
|
| 462 |
+
setuptools_flat_installation = (
|
| 463 |
+
dist.installed_with_setuptools_egg_info
|
| 464 |
+
and info_location is not None
|
| 465 |
+
and os.path.exists(info_location)
|
| 466 |
+
# If dist is editable and the location points to a ``.egg-info``,
|
| 467 |
+
# we are in fact in the legacy editable case.
|
| 468 |
+
and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
# Uninstall cases order do matter as in the case of 2 installs of the
|
| 472 |
+
# same package, pip needs to uninstall the currently detected version
|
| 473 |
+
if setuptools_flat_installation:
|
| 474 |
+
if info_location is not None:
|
| 475 |
+
paths_to_remove.add(info_location)
|
| 476 |
+
installed_files = dist.iter_declared_entries()
|
| 477 |
+
if installed_files is not None:
|
| 478 |
+
for installed_file in installed_files:
|
| 479 |
+
paths_to_remove.add(os.path.join(dist_location, installed_file))
|
| 480 |
+
# FIXME: need a test for this elif block
|
| 481 |
+
# occurs with --single-version-externally-managed/--record outside
|
| 482 |
+
# of pip
|
| 483 |
+
elif dist.is_file("top_level.txt"):
|
| 484 |
+
try:
|
| 485 |
+
namespace_packages = dist.read_text("namespace_packages.txt")
|
| 486 |
+
except FileNotFoundError:
|
| 487 |
+
namespaces = []
|
| 488 |
+
else:
|
| 489 |
+
namespaces = namespace_packages.splitlines(keepends=False)
|
| 490 |
+
for top_level_pkg in [
|
| 491 |
+
p
|
| 492 |
+
for p in dist.read_text("top_level.txt").splitlines()
|
| 493 |
+
if p and p not in namespaces
|
| 494 |
+
]:
|
| 495 |
+
path = os.path.join(dist_location, top_level_pkg)
|
| 496 |
+
paths_to_remove.add(path)
|
| 497 |
+
paths_to_remove.add(f"{path}.py")
|
| 498 |
+
paths_to_remove.add(f"{path}.pyc")
|
| 499 |
+
paths_to_remove.add(f"{path}.pyo")
|
| 500 |
+
|
| 501 |
+
elif dist.installed_by_distutils:
|
| 502 |
+
raise LegacyDistutilsInstall(distribution=dist)
|
| 503 |
+
|
| 504 |
+
elif dist.installed_as_egg:
|
| 505 |
+
# package installed by easy_install
|
| 506 |
+
# We cannot match on dist.egg_name because it can slightly vary
|
| 507 |
+
# i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
|
| 508 |
+
# XXX We use normalized_dist_location because dist_location my contain
|
| 509 |
+
# a trailing / if the distribution is a zipped egg
|
| 510 |
+
# (which is not a directory).
|
| 511 |
+
paths_to_remove.add(normalized_dist_location)
|
| 512 |
+
easy_install_egg = os.path.split(normalized_dist_location)[1]
|
| 513 |
+
easy_install_pth = os.path.join(
|
| 514 |
+
os.path.dirname(normalized_dist_location),
|
| 515 |
+
"easy-install.pth",
|
| 516 |
+
)
|
| 517 |
+
paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
|
| 518 |
+
|
| 519 |
+
elif dist.installed_with_dist_info:
|
| 520 |
+
for path in uninstallation_paths(dist):
|
| 521 |
+
paths_to_remove.add(path)
|
| 522 |
+
|
| 523 |
+
elif develop_egg_link:
|
| 524 |
+
# PEP 660 modern editable is handled in the ``.dist-info`` case
|
| 525 |
+
# above, so this only covers the setuptools-style editable.
|
| 526 |
+
with open(develop_egg_link) as fh:
|
| 527 |
+
link_pointer = os.path.normcase(fh.readline().strip())
|
| 528 |
+
normalized_link_pointer = paths_to_remove._normalize_path_cached(
|
| 529 |
+
link_pointer
|
| 530 |
+
)
|
| 531 |
+
assert os.path.samefile(
|
| 532 |
+
normalized_link_pointer, normalized_dist_location
|
| 533 |
+
), (
|
| 534 |
+
f"Egg-link {develop_egg_link} (to {link_pointer}) does not match "
|
| 535 |
+
f"installed location of {dist.raw_name} (at {dist_location})"
|
| 536 |
+
)
|
| 537 |
+
paths_to_remove.add(develop_egg_link)
|
| 538 |
+
easy_install_pth = os.path.join(
|
| 539 |
+
os.path.dirname(develop_egg_link), "easy-install.pth"
|
| 540 |
+
)
|
| 541 |
+
paths_to_remove.add_pth(easy_install_pth, dist_location)
|
| 542 |
+
|
| 543 |
+
else:
|
| 544 |
+
logger.debug(
|
| 545 |
+
"Not sure how to uninstall: %s - Check: %s",
|
| 546 |
+
dist,
|
| 547 |
+
dist_location,
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
if dist.in_usersite:
|
| 551 |
+
bin_dir = get_bin_user()
|
| 552 |
+
else:
|
| 553 |
+
bin_dir = get_bin_prefix()
|
| 554 |
+
|
| 555 |
+
# find distutils scripts= scripts
|
| 556 |
+
try:
|
| 557 |
+
for script in dist.iter_distutils_script_names():
|
| 558 |
+
paths_to_remove.add(os.path.join(bin_dir, script))
|
| 559 |
+
if WINDOWS:
|
| 560 |
+
paths_to_remove.add(os.path.join(bin_dir, f"{script}.bat"))
|
| 561 |
+
except (FileNotFoundError, NotADirectoryError):
|
| 562 |
+
pass
|
| 563 |
+
|
| 564 |
+
# find console_scripts and gui_scripts
|
| 565 |
+
def iter_scripts_to_remove(
|
| 566 |
+
dist: BaseDistribution,
|
| 567 |
+
bin_dir: str,
|
| 568 |
+
) -> Generator[str, None, None]:
|
| 569 |
+
for entry_point in dist.iter_entry_points():
|
| 570 |
+
if entry_point.group == "console_scripts":
|
| 571 |
+
yield from _script_names(bin_dir, entry_point.name, False)
|
| 572 |
+
elif entry_point.group == "gui_scripts":
|
| 573 |
+
yield from _script_names(bin_dir, entry_point.name, True)
|
| 574 |
+
|
| 575 |
+
for s in iter_scripts_to_remove(dist, bin_dir):
|
| 576 |
+
paths_to_remove.add(s)
|
| 577 |
+
|
| 578 |
+
return paths_to_remove
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
class UninstallPthEntries:
|
| 582 |
+
def __init__(self, pth_file: str) -> None:
|
| 583 |
+
self.file = pth_file
|
| 584 |
+
self.entries: Set[str] = set()
|
| 585 |
+
self._saved_lines: Optional[List[bytes]] = None
|
| 586 |
+
|
| 587 |
+
def add(self, entry: str) -> None:
|
| 588 |
+
entry = os.path.normcase(entry)
|
| 589 |
+
# On Windows, os.path.normcase converts the entry to use
|
| 590 |
+
# backslashes. This is correct for entries that describe absolute
|
| 591 |
+
# paths outside of site-packages, but all the others use forward
|
| 592 |
+
# slashes.
|
| 593 |
+
# os.path.splitdrive is used instead of os.path.isabs because isabs
|
| 594 |
+
# treats non-absolute paths with drive letter markings like c:foo\bar
|
| 595 |
+
# as absolute paths. It also does not recognize UNC paths if they don't
|
| 596 |
+
# have more than "\\sever\share". Valid examples: "\\server\share\" or
|
| 597 |
+
# "\\server\share\folder".
|
| 598 |
+
if WINDOWS and not os.path.splitdrive(entry)[0]:
|
| 599 |
+
entry = entry.replace("\\", "/")
|
| 600 |
+
self.entries.add(entry)
|
| 601 |
+
|
| 602 |
+
def remove(self) -> None:
|
| 603 |
+
logger.verbose("Removing pth entries from %s:", self.file)
|
| 604 |
+
|
| 605 |
+
# If the file doesn't exist, log a warning and return
|
| 606 |
+
if not os.path.isfile(self.file):
|
| 607 |
+
logger.warning("Cannot remove entries from nonexistent file %s", self.file)
|
| 608 |
+
return
|
| 609 |
+
with open(self.file, "rb") as fh:
|
| 610 |
+
# windows uses '\r\n' with py3k, but uses '\n' with py2.x
|
| 611 |
+
lines = fh.readlines()
|
| 612 |
+
self._saved_lines = lines
|
| 613 |
+
if any(b"\r\n" in line for line in lines):
|
| 614 |
+
endline = "\r\n"
|
| 615 |
+
else:
|
| 616 |
+
endline = "\n"
|
| 617 |
+
# handle missing trailing newline
|
| 618 |
+
if lines and not lines[-1].endswith(endline.encode("utf-8")):
|
| 619 |
+
lines[-1] = lines[-1] + endline.encode("utf-8")
|
| 620 |
+
for entry in self.entries:
|
| 621 |
+
try:
|
| 622 |
+
logger.verbose("Removing entry: %s", entry)
|
| 623 |
+
lines.remove((entry + endline).encode("utf-8"))
|
| 624 |
+
except ValueError:
|
| 625 |
+
pass
|
| 626 |
+
with open(self.file, "wb") as fh:
|
| 627 |
+
fh.writelines(lines)
|
| 628 |
+
|
| 629 |
+
def rollback(self) -> bool:
|
| 630 |
+
if self._saved_lines is None:
|
| 631 |
+
logger.error("Cannot roll back changes to %s, none were made", self.file)
|
| 632 |
+
return False
|
| 633 |
+
logger.debug("Rolling %s back to previous state", self.file)
|
| 634 |
+
with open(self.file, "wb") as fh:
|
| 635 |
+
fh.writelines(self._saved_lines)
|
| 636 |
+
return True
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/__init__.py
ADDED
|
File without changes
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (201 Bytes). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/__pycache__/base.cpython-313.pyc
ADDED
|
Binary file (1.25 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/base.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable, List, Optional
|
| 2 |
+
|
| 3 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 4 |
+
from pip._internal.req.req_set import RequirementSet
|
| 5 |
+
|
| 6 |
+
InstallRequirementProvider = Callable[
|
| 7 |
+
[str, Optional[InstallRequirement]], InstallRequirement
|
| 8 |
+
]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class BaseResolver:
|
| 12 |
+
def resolve(
|
| 13 |
+
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
|
| 14 |
+
) -> RequirementSet:
|
| 15 |
+
raise NotImplementedError()
|
| 16 |
+
|
| 17 |
+
def get_installation_order(
|
| 18 |
+
self, req_set: RequirementSet
|
| 19 |
+
) -> List[InstallRequirement]:
|
| 20 |
+
raise NotImplementedError()
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__init__.py
ADDED
|
File without changes
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (208 Bytes). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/__pycache__/resolver.cpython-313.pyc
ADDED
|
Binary file (22.9 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/legacy/resolver.py
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dependency Resolution
|
| 2 |
+
|
| 3 |
+
The dependency resolution in pip is performed as follows:
|
| 4 |
+
|
| 5 |
+
for top-level requirements:
|
| 6 |
+
a. only one spec allowed per project, regardless of conflicts or not.
|
| 7 |
+
otherwise a "double requirement" exception is raised
|
| 8 |
+
b. they override sub-dependency requirements.
|
| 9 |
+
for sub-dependencies
|
| 10 |
+
a. "first found, wins" (where the order is breadth first)
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import logging
|
| 14 |
+
import sys
|
| 15 |
+
from collections import defaultdict
|
| 16 |
+
from itertools import chain
|
| 17 |
+
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
|
| 18 |
+
|
| 19 |
+
from pip._vendor.packaging import specifiers
|
| 20 |
+
from pip._vendor.packaging.requirements import Requirement
|
| 21 |
+
|
| 22 |
+
from pip._internal.cache import WheelCache
|
| 23 |
+
from pip._internal.exceptions import (
|
| 24 |
+
BestVersionAlreadyInstalled,
|
| 25 |
+
DistributionNotFound,
|
| 26 |
+
HashError,
|
| 27 |
+
HashErrors,
|
| 28 |
+
InstallationError,
|
| 29 |
+
NoneMetadataError,
|
| 30 |
+
UnsupportedPythonVersion,
|
| 31 |
+
)
|
| 32 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 33 |
+
from pip._internal.metadata import BaseDistribution
|
| 34 |
+
from pip._internal.models.link import Link
|
| 35 |
+
from pip._internal.models.wheel import Wheel
|
| 36 |
+
from pip._internal.operations.prepare import RequirementPreparer
|
| 37 |
+
from pip._internal.req.req_install import (
|
| 38 |
+
InstallRequirement,
|
| 39 |
+
check_invalid_constraint_type,
|
| 40 |
+
)
|
| 41 |
+
from pip._internal.req.req_set import RequirementSet
|
| 42 |
+
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
|
| 43 |
+
from pip._internal.utils import compatibility_tags
|
| 44 |
+
from pip._internal.utils.compatibility_tags import get_supported
|
| 45 |
+
from pip._internal.utils.direct_url_helpers import direct_url_from_link
|
| 46 |
+
from pip._internal.utils.logging import indent_log
|
| 47 |
+
from pip._internal.utils.misc import normalize_version_info
|
| 48 |
+
from pip._internal.utils.packaging import check_requires_python
|
| 49 |
+
|
| 50 |
+
logger = logging.getLogger(__name__)
|
| 51 |
+
|
| 52 |
+
DiscoveredDependencies = DefaultDict[Optional[str], List[InstallRequirement]]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _check_dist_requires_python(
|
| 56 |
+
dist: BaseDistribution,
|
| 57 |
+
version_info: Tuple[int, int, int],
|
| 58 |
+
ignore_requires_python: bool = False,
|
| 59 |
+
) -> None:
|
| 60 |
+
"""
|
| 61 |
+
Check whether the given Python version is compatible with a distribution's
|
| 62 |
+
"Requires-Python" value.
|
| 63 |
+
|
| 64 |
+
:param version_info: A 3-tuple of ints representing the Python
|
| 65 |
+
major-minor-micro version to check.
|
| 66 |
+
:param ignore_requires_python: Whether to ignore the "Requires-Python"
|
| 67 |
+
value if the given Python version isn't compatible.
|
| 68 |
+
|
| 69 |
+
:raises UnsupportedPythonVersion: When the given Python version isn't
|
| 70 |
+
compatible.
|
| 71 |
+
"""
|
| 72 |
+
# This idiosyncratically converts the SpecifierSet to str and let
|
| 73 |
+
# check_requires_python then parse it again into SpecifierSet. But this
|
| 74 |
+
# is the legacy resolver so I'm just not going to bother refactoring.
|
| 75 |
+
try:
|
| 76 |
+
requires_python = str(dist.requires_python)
|
| 77 |
+
except FileNotFoundError as e:
|
| 78 |
+
raise NoneMetadataError(dist, str(e))
|
| 79 |
+
try:
|
| 80 |
+
is_compatible = check_requires_python(
|
| 81 |
+
requires_python,
|
| 82 |
+
version_info=version_info,
|
| 83 |
+
)
|
| 84 |
+
except specifiers.InvalidSpecifier as exc:
|
| 85 |
+
logger.warning(
|
| 86 |
+
"Package %r has an invalid Requires-Python: %s", dist.raw_name, exc
|
| 87 |
+
)
|
| 88 |
+
return
|
| 89 |
+
|
| 90 |
+
if is_compatible:
|
| 91 |
+
return
|
| 92 |
+
|
| 93 |
+
version = ".".join(map(str, version_info))
|
| 94 |
+
if ignore_requires_python:
|
| 95 |
+
logger.debug(
|
| 96 |
+
"Ignoring failed Requires-Python check for package %r: %s not in %r",
|
| 97 |
+
dist.raw_name,
|
| 98 |
+
version,
|
| 99 |
+
requires_python,
|
| 100 |
+
)
|
| 101 |
+
return
|
| 102 |
+
|
| 103 |
+
raise UnsupportedPythonVersion(
|
| 104 |
+
f"Package {dist.raw_name!r} requires a different Python: "
|
| 105 |
+
f"{version} not in {requires_python!r}"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class Resolver(BaseResolver):
|
| 110 |
+
"""Resolves which packages need to be installed/uninstalled to perform \
|
| 111 |
+
the requested operation without breaking the requirements of any package.
|
| 112 |
+
"""
|
| 113 |
+
|
| 114 |
+
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
preparer: RequirementPreparer,
|
| 119 |
+
finder: PackageFinder,
|
| 120 |
+
wheel_cache: Optional[WheelCache],
|
| 121 |
+
make_install_req: InstallRequirementProvider,
|
| 122 |
+
use_user_site: bool,
|
| 123 |
+
ignore_dependencies: bool,
|
| 124 |
+
ignore_installed: bool,
|
| 125 |
+
ignore_requires_python: bool,
|
| 126 |
+
force_reinstall: bool,
|
| 127 |
+
upgrade_strategy: str,
|
| 128 |
+
py_version_info: Optional[Tuple[int, ...]] = None,
|
| 129 |
+
) -> None:
|
| 130 |
+
super().__init__()
|
| 131 |
+
assert upgrade_strategy in self._allowed_strategies
|
| 132 |
+
|
| 133 |
+
if py_version_info is None:
|
| 134 |
+
py_version_info = sys.version_info[:3]
|
| 135 |
+
else:
|
| 136 |
+
py_version_info = normalize_version_info(py_version_info)
|
| 137 |
+
|
| 138 |
+
self._py_version_info = py_version_info
|
| 139 |
+
|
| 140 |
+
self.preparer = preparer
|
| 141 |
+
self.finder = finder
|
| 142 |
+
self.wheel_cache = wheel_cache
|
| 143 |
+
|
| 144 |
+
self.upgrade_strategy = upgrade_strategy
|
| 145 |
+
self.force_reinstall = force_reinstall
|
| 146 |
+
self.ignore_dependencies = ignore_dependencies
|
| 147 |
+
self.ignore_installed = ignore_installed
|
| 148 |
+
self.ignore_requires_python = ignore_requires_python
|
| 149 |
+
self.use_user_site = use_user_site
|
| 150 |
+
self._make_install_req = make_install_req
|
| 151 |
+
|
| 152 |
+
self._discovered_dependencies: DiscoveredDependencies = defaultdict(list)
|
| 153 |
+
|
| 154 |
+
def resolve(
|
| 155 |
+
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
|
| 156 |
+
) -> RequirementSet:
|
| 157 |
+
"""Resolve what operations need to be done
|
| 158 |
+
|
| 159 |
+
As a side-effect of this method, the packages (and their dependencies)
|
| 160 |
+
are downloaded, unpacked and prepared for installation. This
|
| 161 |
+
preparation is done by ``pip.operations.prepare``.
|
| 162 |
+
|
| 163 |
+
Once PyPI has static dependency metadata available, it would be
|
| 164 |
+
possible to move the preparation to become a step separated from
|
| 165 |
+
dependency resolution.
|
| 166 |
+
"""
|
| 167 |
+
requirement_set = RequirementSet(check_supported_wheels=check_supported_wheels)
|
| 168 |
+
for req in root_reqs:
|
| 169 |
+
if req.constraint:
|
| 170 |
+
check_invalid_constraint_type(req)
|
| 171 |
+
self._add_requirement_to_set(requirement_set, req)
|
| 172 |
+
|
| 173 |
+
# Actually prepare the files, and collect any exceptions. Most hash
|
| 174 |
+
# exceptions cannot be checked ahead of time, because
|
| 175 |
+
# _populate_link() needs to be called before we can make decisions
|
| 176 |
+
# based on link type.
|
| 177 |
+
discovered_reqs: List[InstallRequirement] = []
|
| 178 |
+
hash_errors = HashErrors()
|
| 179 |
+
for req in chain(requirement_set.all_requirements, discovered_reqs):
|
| 180 |
+
try:
|
| 181 |
+
discovered_reqs.extend(self._resolve_one(requirement_set, req))
|
| 182 |
+
except HashError as exc:
|
| 183 |
+
exc.req = req
|
| 184 |
+
hash_errors.append(exc)
|
| 185 |
+
|
| 186 |
+
if hash_errors:
|
| 187 |
+
raise hash_errors
|
| 188 |
+
|
| 189 |
+
return requirement_set
|
| 190 |
+
|
| 191 |
+
def _add_requirement_to_set(
|
| 192 |
+
self,
|
| 193 |
+
requirement_set: RequirementSet,
|
| 194 |
+
install_req: InstallRequirement,
|
| 195 |
+
parent_req_name: Optional[str] = None,
|
| 196 |
+
extras_requested: Optional[Iterable[str]] = None,
|
| 197 |
+
) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]]:
|
| 198 |
+
"""Add install_req as a requirement to install.
|
| 199 |
+
|
| 200 |
+
:param parent_req_name: The name of the requirement that needed this
|
| 201 |
+
added. The name is used because when multiple unnamed requirements
|
| 202 |
+
resolve to the same name, we could otherwise end up with dependency
|
| 203 |
+
links that point outside the Requirements set. parent_req must
|
| 204 |
+
already be added. Note that None implies that this is a user
|
| 205 |
+
supplied requirement, vs an inferred one.
|
| 206 |
+
:param extras_requested: an iterable of extras used to evaluate the
|
| 207 |
+
environment markers.
|
| 208 |
+
:return: Additional requirements to scan. That is either [] if
|
| 209 |
+
the requirement is not applicable, or [install_req] if the
|
| 210 |
+
requirement is applicable and has just been added.
|
| 211 |
+
"""
|
| 212 |
+
# If the markers do not match, ignore this requirement.
|
| 213 |
+
if not install_req.match_markers(extras_requested):
|
| 214 |
+
logger.info(
|
| 215 |
+
"Ignoring %s: markers '%s' don't match your environment",
|
| 216 |
+
install_req.name,
|
| 217 |
+
install_req.markers,
|
| 218 |
+
)
|
| 219 |
+
return [], None
|
| 220 |
+
|
| 221 |
+
# If the wheel is not supported, raise an error.
|
| 222 |
+
# Should check this after filtering out based on environment markers to
|
| 223 |
+
# allow specifying different wheels based on the environment/OS, in a
|
| 224 |
+
# single requirements file.
|
| 225 |
+
if install_req.link and install_req.link.is_wheel:
|
| 226 |
+
wheel = Wheel(install_req.link.filename)
|
| 227 |
+
tags = compatibility_tags.get_supported()
|
| 228 |
+
if requirement_set.check_supported_wheels and not wheel.supported(tags):
|
| 229 |
+
raise InstallationError(
|
| 230 |
+
f"{wheel.filename} is not a supported wheel on this platform."
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# This next bit is really a sanity check.
|
| 234 |
+
assert (
|
| 235 |
+
not install_req.user_supplied or parent_req_name is None
|
| 236 |
+
), "a user supplied req shouldn't have a parent"
|
| 237 |
+
|
| 238 |
+
# Unnamed requirements are scanned again and the requirement won't be
|
| 239 |
+
# added as a dependency until after scanning.
|
| 240 |
+
if not install_req.name:
|
| 241 |
+
requirement_set.add_unnamed_requirement(install_req)
|
| 242 |
+
return [install_req], None
|
| 243 |
+
|
| 244 |
+
try:
|
| 245 |
+
existing_req: Optional[InstallRequirement] = (
|
| 246 |
+
requirement_set.get_requirement(install_req.name)
|
| 247 |
+
)
|
| 248 |
+
except KeyError:
|
| 249 |
+
existing_req = None
|
| 250 |
+
|
| 251 |
+
has_conflicting_requirement = (
|
| 252 |
+
parent_req_name is None
|
| 253 |
+
and existing_req
|
| 254 |
+
and not existing_req.constraint
|
| 255 |
+
and existing_req.extras == install_req.extras
|
| 256 |
+
and existing_req.req
|
| 257 |
+
and install_req.req
|
| 258 |
+
and existing_req.req.specifier != install_req.req.specifier
|
| 259 |
+
)
|
| 260 |
+
if has_conflicting_requirement:
|
| 261 |
+
raise InstallationError(
|
| 262 |
+
f"Double requirement given: {install_req} "
|
| 263 |
+
f"(already in {existing_req}, name={install_req.name!r})"
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
# When no existing requirement exists, add the requirement as a
|
| 267 |
+
# dependency and it will be scanned again after.
|
| 268 |
+
if not existing_req:
|
| 269 |
+
requirement_set.add_named_requirement(install_req)
|
| 270 |
+
# We'd want to rescan this requirement later
|
| 271 |
+
return [install_req], install_req
|
| 272 |
+
|
| 273 |
+
# Assume there's no need to scan, and that we've already
|
| 274 |
+
# encountered this for scanning.
|
| 275 |
+
if install_req.constraint or not existing_req.constraint:
|
| 276 |
+
return [], existing_req
|
| 277 |
+
|
| 278 |
+
does_not_satisfy_constraint = install_req.link and not (
|
| 279 |
+
existing_req.link and install_req.link.path == existing_req.link.path
|
| 280 |
+
)
|
| 281 |
+
if does_not_satisfy_constraint:
|
| 282 |
+
raise InstallationError(
|
| 283 |
+
f"Could not satisfy constraints for '{install_req.name}': "
|
| 284 |
+
"installation from path or url cannot be "
|
| 285 |
+
"constrained to a version"
|
| 286 |
+
)
|
| 287 |
+
# If we're now installing a constraint, mark the existing
|
| 288 |
+
# object for real installation.
|
| 289 |
+
existing_req.constraint = False
|
| 290 |
+
# If we're now installing a user supplied requirement,
|
| 291 |
+
# mark the existing object as such.
|
| 292 |
+
if install_req.user_supplied:
|
| 293 |
+
existing_req.user_supplied = True
|
| 294 |
+
existing_req.extras = tuple(
|
| 295 |
+
sorted(set(existing_req.extras) | set(install_req.extras))
|
| 296 |
+
)
|
| 297 |
+
logger.debug(
|
| 298 |
+
"Setting %s extras to: %s",
|
| 299 |
+
existing_req,
|
| 300 |
+
existing_req.extras,
|
| 301 |
+
)
|
| 302 |
+
# Return the existing requirement for addition to the parent and
|
| 303 |
+
# scanning again.
|
| 304 |
+
return [existing_req], existing_req
|
| 305 |
+
|
| 306 |
+
def _is_upgrade_allowed(self, req: InstallRequirement) -> bool:
|
| 307 |
+
if self.upgrade_strategy == "to-satisfy-only":
|
| 308 |
+
return False
|
| 309 |
+
elif self.upgrade_strategy == "eager":
|
| 310 |
+
return True
|
| 311 |
+
else:
|
| 312 |
+
assert self.upgrade_strategy == "only-if-needed"
|
| 313 |
+
return req.user_supplied or req.constraint
|
| 314 |
+
|
| 315 |
+
def _set_req_to_reinstall(self, req: InstallRequirement) -> None:
|
| 316 |
+
"""
|
| 317 |
+
Set a requirement to be installed.
|
| 318 |
+
"""
|
| 319 |
+
# Don't uninstall the conflict if doing a user install and the
|
| 320 |
+
# conflict is not a user install.
|
| 321 |
+
assert req.satisfied_by is not None
|
| 322 |
+
if not self.use_user_site or req.satisfied_by.in_usersite:
|
| 323 |
+
req.should_reinstall = True
|
| 324 |
+
req.satisfied_by = None
|
| 325 |
+
|
| 326 |
+
def _check_skip_installed(
|
| 327 |
+
self, req_to_install: InstallRequirement
|
| 328 |
+
) -> Optional[str]:
|
| 329 |
+
"""Check if req_to_install should be skipped.
|
| 330 |
+
|
| 331 |
+
This will check if the req is installed, and whether we should upgrade
|
| 332 |
+
or reinstall it, taking into account all the relevant user options.
|
| 333 |
+
|
| 334 |
+
After calling this req_to_install will only have satisfied_by set to
|
| 335 |
+
None if the req_to_install is to be upgraded/reinstalled etc. Any
|
| 336 |
+
other value will be a dist recording the current thing installed that
|
| 337 |
+
satisfies the requirement.
|
| 338 |
+
|
| 339 |
+
Note that for vcs urls and the like we can't assess skipping in this
|
| 340 |
+
routine - we simply identify that we need to pull the thing down,
|
| 341 |
+
then later on it is pulled down and introspected to assess upgrade/
|
| 342 |
+
reinstalls etc.
|
| 343 |
+
|
| 344 |
+
:return: A text reason for why it was skipped, or None.
|
| 345 |
+
"""
|
| 346 |
+
if self.ignore_installed:
|
| 347 |
+
return None
|
| 348 |
+
|
| 349 |
+
req_to_install.check_if_exists(self.use_user_site)
|
| 350 |
+
if not req_to_install.satisfied_by:
|
| 351 |
+
return None
|
| 352 |
+
|
| 353 |
+
if self.force_reinstall:
|
| 354 |
+
self._set_req_to_reinstall(req_to_install)
|
| 355 |
+
return None
|
| 356 |
+
|
| 357 |
+
if not self._is_upgrade_allowed(req_to_install):
|
| 358 |
+
if self.upgrade_strategy == "only-if-needed":
|
| 359 |
+
return "already satisfied, skipping upgrade"
|
| 360 |
+
return "already satisfied"
|
| 361 |
+
|
| 362 |
+
# Check for the possibility of an upgrade. For link-based
|
| 363 |
+
# requirements we have to pull the tree down and inspect to assess
|
| 364 |
+
# the version #, so it's handled way down.
|
| 365 |
+
if not req_to_install.link:
|
| 366 |
+
try:
|
| 367 |
+
self.finder.find_requirement(req_to_install, upgrade=True)
|
| 368 |
+
except BestVersionAlreadyInstalled:
|
| 369 |
+
# Then the best version is installed.
|
| 370 |
+
return "already up-to-date"
|
| 371 |
+
except DistributionNotFound:
|
| 372 |
+
# No distribution found, so we squash the error. It will
|
| 373 |
+
# be raised later when we re-try later to do the install.
|
| 374 |
+
# Why don't we just raise here?
|
| 375 |
+
pass
|
| 376 |
+
|
| 377 |
+
self._set_req_to_reinstall(req_to_install)
|
| 378 |
+
return None
|
| 379 |
+
|
| 380 |
+
def _find_requirement_link(self, req: InstallRequirement) -> Optional[Link]:
|
| 381 |
+
upgrade = self._is_upgrade_allowed(req)
|
| 382 |
+
best_candidate = self.finder.find_requirement(req, upgrade)
|
| 383 |
+
if not best_candidate:
|
| 384 |
+
return None
|
| 385 |
+
|
| 386 |
+
# Log a warning per PEP 592 if necessary before returning.
|
| 387 |
+
link = best_candidate.link
|
| 388 |
+
if link.is_yanked:
|
| 389 |
+
reason = link.yanked_reason or "<none given>"
|
| 390 |
+
msg = (
|
| 391 |
+
# Mark this as a unicode string to prevent
|
| 392 |
+
# "UnicodeEncodeError: 'ascii' codec can't encode character"
|
| 393 |
+
# in Python 2 when the reason contains non-ascii characters.
|
| 394 |
+
"The candidate selected for download or install is a "
|
| 395 |
+
f"yanked version: {best_candidate}\n"
|
| 396 |
+
f"Reason for being yanked: {reason}"
|
| 397 |
+
)
|
| 398 |
+
logger.warning(msg)
|
| 399 |
+
|
| 400 |
+
return link
|
| 401 |
+
|
| 402 |
+
def _populate_link(self, req: InstallRequirement) -> None:
|
| 403 |
+
"""Ensure that if a link can be found for this, that it is found.
|
| 404 |
+
|
| 405 |
+
Note that req.link may still be None - if the requirement is already
|
| 406 |
+
installed and not needed to be upgraded based on the return value of
|
| 407 |
+
_is_upgrade_allowed().
|
| 408 |
+
|
| 409 |
+
If preparer.require_hashes is True, don't use the wheel cache, because
|
| 410 |
+
cached wheels, always built locally, have different hashes than the
|
| 411 |
+
files downloaded from the index server and thus throw false hash
|
| 412 |
+
mismatches. Furthermore, cached wheels at present have undeterministic
|
| 413 |
+
contents due to file modification times.
|
| 414 |
+
"""
|
| 415 |
+
if req.link is None:
|
| 416 |
+
req.link = self._find_requirement_link(req)
|
| 417 |
+
|
| 418 |
+
if self.wheel_cache is None or self.preparer.require_hashes:
|
| 419 |
+
return
|
| 420 |
+
|
| 421 |
+
assert req.link is not None, "_find_requirement_link unexpectedly returned None"
|
| 422 |
+
cache_entry = self.wheel_cache.get_cache_entry(
|
| 423 |
+
link=req.link,
|
| 424 |
+
package_name=req.name,
|
| 425 |
+
supported_tags=get_supported(),
|
| 426 |
+
)
|
| 427 |
+
if cache_entry is not None:
|
| 428 |
+
logger.debug("Using cached wheel link: %s", cache_entry.link)
|
| 429 |
+
if req.link is req.original_link and cache_entry.persistent:
|
| 430 |
+
req.cached_wheel_source_link = req.link
|
| 431 |
+
if cache_entry.origin is not None:
|
| 432 |
+
req.download_info = cache_entry.origin
|
| 433 |
+
else:
|
| 434 |
+
# Legacy cache entry that does not have origin.json.
|
| 435 |
+
# download_info may miss the archive_info.hashes field.
|
| 436 |
+
req.download_info = direct_url_from_link(
|
| 437 |
+
req.link, link_is_in_wheel_cache=cache_entry.persistent
|
| 438 |
+
)
|
| 439 |
+
req.link = cache_entry.link
|
| 440 |
+
|
| 441 |
+
def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:
|
| 442 |
+
"""Takes a InstallRequirement and returns a single AbstractDist \
|
| 443 |
+
representing a prepared variant of the same.
|
| 444 |
+
"""
|
| 445 |
+
if req.editable:
|
| 446 |
+
return self.preparer.prepare_editable_requirement(req)
|
| 447 |
+
|
| 448 |
+
# satisfied_by is only evaluated by calling _check_skip_installed,
|
| 449 |
+
# so it must be None here.
|
| 450 |
+
assert req.satisfied_by is None
|
| 451 |
+
skip_reason = self._check_skip_installed(req)
|
| 452 |
+
|
| 453 |
+
if req.satisfied_by:
|
| 454 |
+
return self.preparer.prepare_installed_requirement(req, skip_reason)
|
| 455 |
+
|
| 456 |
+
# We eagerly populate the link, since that's our "legacy" behavior.
|
| 457 |
+
self._populate_link(req)
|
| 458 |
+
dist = self.preparer.prepare_linked_requirement(req)
|
| 459 |
+
|
| 460 |
+
# NOTE
|
| 461 |
+
# The following portion is for determining if a certain package is
|
| 462 |
+
# going to be re-installed/upgraded or not and reporting to the user.
|
| 463 |
+
# This should probably get cleaned up in a future refactor.
|
| 464 |
+
|
| 465 |
+
# req.req is only avail after unpack for URL
|
| 466 |
+
# pkgs repeat check_if_exists to uninstall-on-upgrade
|
| 467 |
+
# (#14)
|
| 468 |
+
if not self.ignore_installed:
|
| 469 |
+
req.check_if_exists(self.use_user_site)
|
| 470 |
+
|
| 471 |
+
if req.satisfied_by:
|
| 472 |
+
should_modify = (
|
| 473 |
+
self.upgrade_strategy != "to-satisfy-only"
|
| 474 |
+
or self.force_reinstall
|
| 475 |
+
or self.ignore_installed
|
| 476 |
+
or req.link.scheme == "file"
|
| 477 |
+
)
|
| 478 |
+
if should_modify:
|
| 479 |
+
self._set_req_to_reinstall(req)
|
| 480 |
+
else:
|
| 481 |
+
logger.info(
|
| 482 |
+
"Requirement already satisfied (use --upgrade to upgrade): %s",
|
| 483 |
+
req,
|
| 484 |
+
)
|
| 485 |
+
return dist
|
| 486 |
+
|
| 487 |
+
def _resolve_one(
|
| 488 |
+
self,
|
| 489 |
+
requirement_set: RequirementSet,
|
| 490 |
+
req_to_install: InstallRequirement,
|
| 491 |
+
) -> List[InstallRequirement]:
|
| 492 |
+
"""Prepare a single requirements file.
|
| 493 |
+
|
| 494 |
+
:return: A list of additional InstallRequirements to also install.
|
| 495 |
+
"""
|
| 496 |
+
# Tell user what we are doing for this requirement:
|
| 497 |
+
# obtain (editable), skipping, processing (local url), collecting
|
| 498 |
+
# (remote url or package name)
|
| 499 |
+
if req_to_install.constraint or req_to_install.prepared:
|
| 500 |
+
return []
|
| 501 |
+
|
| 502 |
+
req_to_install.prepared = True
|
| 503 |
+
|
| 504 |
+
# Parse and return dependencies
|
| 505 |
+
dist = self._get_dist_for(req_to_install)
|
| 506 |
+
# This will raise UnsupportedPythonVersion if the given Python
|
| 507 |
+
# version isn't compatible with the distribution's Requires-Python.
|
| 508 |
+
_check_dist_requires_python(
|
| 509 |
+
dist,
|
| 510 |
+
version_info=self._py_version_info,
|
| 511 |
+
ignore_requires_python=self.ignore_requires_python,
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
more_reqs: List[InstallRequirement] = []
|
| 515 |
+
|
| 516 |
+
def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
|
| 517 |
+
# This idiosyncratically converts the Requirement to str and let
|
| 518 |
+
# make_install_req then parse it again into Requirement. But this is
|
| 519 |
+
# the legacy resolver so I'm just not going to bother refactoring.
|
| 520 |
+
sub_install_req = self._make_install_req(str(subreq), req_to_install)
|
| 521 |
+
parent_req_name = req_to_install.name
|
| 522 |
+
to_scan_again, add_to_parent = self._add_requirement_to_set(
|
| 523 |
+
requirement_set,
|
| 524 |
+
sub_install_req,
|
| 525 |
+
parent_req_name=parent_req_name,
|
| 526 |
+
extras_requested=extras_requested,
|
| 527 |
+
)
|
| 528 |
+
if parent_req_name and add_to_parent:
|
| 529 |
+
self._discovered_dependencies[parent_req_name].append(add_to_parent)
|
| 530 |
+
more_reqs.extend(to_scan_again)
|
| 531 |
+
|
| 532 |
+
with indent_log():
|
| 533 |
+
# We add req_to_install before its dependencies, so that we
|
| 534 |
+
# can refer to it when adding dependencies.
|
| 535 |
+
assert req_to_install.name is not None
|
| 536 |
+
if not requirement_set.has_requirement(req_to_install.name):
|
| 537 |
+
# 'unnamed' requirements will get added here
|
| 538 |
+
# 'unnamed' requirements can only come from being directly
|
| 539 |
+
# provided by the user.
|
| 540 |
+
assert req_to_install.user_supplied
|
| 541 |
+
self._add_requirement_to_set(
|
| 542 |
+
requirement_set, req_to_install, parent_req_name=None
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
if not self.ignore_dependencies:
|
| 546 |
+
if req_to_install.extras:
|
| 547 |
+
logger.debug(
|
| 548 |
+
"Installing extra requirements: %r",
|
| 549 |
+
",".join(req_to_install.extras),
|
| 550 |
+
)
|
| 551 |
+
missing_requested = sorted(
|
| 552 |
+
set(req_to_install.extras) - set(dist.iter_provided_extras())
|
| 553 |
+
)
|
| 554 |
+
for missing in missing_requested:
|
| 555 |
+
logger.warning(
|
| 556 |
+
"%s %s does not provide the extra '%s'",
|
| 557 |
+
dist.raw_name,
|
| 558 |
+
dist.version,
|
| 559 |
+
missing,
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
available_requested = sorted(
|
| 563 |
+
set(dist.iter_provided_extras()) & set(req_to_install.extras)
|
| 564 |
+
)
|
| 565 |
+
for subreq in dist.iter_dependencies(available_requested):
|
| 566 |
+
add_req(subreq, extras_requested=available_requested)
|
| 567 |
+
|
| 568 |
+
return more_reqs
|
| 569 |
+
|
| 570 |
+
def get_installation_order(
|
| 571 |
+
self, req_set: RequirementSet
|
| 572 |
+
) -> List[InstallRequirement]:
|
| 573 |
+
"""Create the installation order.
|
| 574 |
+
|
| 575 |
+
The installation order is topological - requirements are installed
|
| 576 |
+
before the requiring thing. We break cycles at an arbitrary point,
|
| 577 |
+
and make no other guarantees.
|
| 578 |
+
"""
|
| 579 |
+
# The current implementation, which we may change at any point
|
| 580 |
+
# installs the user specified things in the order given, except when
|
| 581 |
+
# dependencies must come earlier to achieve topological order.
|
| 582 |
+
order = []
|
| 583 |
+
ordered_reqs: Set[InstallRequirement] = set()
|
| 584 |
+
|
| 585 |
+
def schedule(req: InstallRequirement) -> None:
|
| 586 |
+
if req.satisfied_by or req in ordered_reqs:
|
| 587 |
+
return
|
| 588 |
+
if req.constraint:
|
| 589 |
+
return
|
| 590 |
+
ordered_reqs.add(req)
|
| 591 |
+
for dep in self._discovered_dependencies[req.name]:
|
| 592 |
+
schedule(dep)
|
| 593 |
+
order.append(req)
|
| 594 |
+
|
| 595 |
+
for install_req in req_set.requirements.values():
|
| 596 |
+
schedule(install_req)
|
| 597 |
+
return order
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__init__.py
ADDED
|
File without changes
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (212 Bytes). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/base.cpython-313.pyc
ADDED
|
Binary file (8.27 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/candidates.cpython-313.pyc
ADDED
|
Binary file (30 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/factory.cpython-313.pyc
ADDED
|
Binary file (33.2 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/found_candidates.cpython-313.pyc
ADDED
|
Binary file (6.84 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/provider.cpython-313.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/reporter.cpython-313.pyc
ADDED
|
Binary file (5.23 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/requirements.cpython-313.pyc
ADDED
|
Binary file (15.8 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/__pycache__/resolver.cpython-313.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/base.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import FrozenSet, Iterable, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
from pip._vendor.packaging.specifiers import SpecifierSet
|
| 5 |
+
from pip._vendor.packaging.utils import NormalizedName
|
| 6 |
+
from pip._vendor.packaging.version import Version
|
| 7 |
+
|
| 8 |
+
from pip._internal.models.link import Link, links_equivalent
|
| 9 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 10 |
+
from pip._internal.utils.hashes import Hashes
|
| 11 |
+
|
| 12 |
+
CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def format_name(project: NormalizedName, extras: FrozenSet[NormalizedName]) -> str:
|
| 16 |
+
if not extras:
|
| 17 |
+
return project
|
| 18 |
+
extras_expr = ",".join(sorted(extras))
|
| 19 |
+
return f"{project}[{extras_expr}]"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@dataclass(frozen=True)
|
| 23 |
+
class Constraint:
|
| 24 |
+
specifier: SpecifierSet
|
| 25 |
+
hashes: Hashes
|
| 26 |
+
links: FrozenSet[Link]
|
| 27 |
+
|
| 28 |
+
@classmethod
|
| 29 |
+
def empty(cls) -> "Constraint":
|
| 30 |
+
return Constraint(SpecifierSet(), Hashes(), frozenset())
|
| 31 |
+
|
| 32 |
+
@classmethod
|
| 33 |
+
def from_ireq(cls, ireq: InstallRequirement) -> "Constraint":
|
| 34 |
+
links = frozenset([ireq.link]) if ireq.link else frozenset()
|
| 35 |
+
return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links)
|
| 36 |
+
|
| 37 |
+
def __bool__(self) -> bool:
|
| 38 |
+
return bool(self.specifier) or bool(self.hashes) or bool(self.links)
|
| 39 |
+
|
| 40 |
+
def __and__(self, other: InstallRequirement) -> "Constraint":
|
| 41 |
+
if not isinstance(other, InstallRequirement):
|
| 42 |
+
return NotImplemented
|
| 43 |
+
specifier = self.specifier & other.specifier
|
| 44 |
+
hashes = self.hashes & other.hashes(trust_internet=False)
|
| 45 |
+
links = self.links
|
| 46 |
+
if other.link:
|
| 47 |
+
links = links.union([other.link])
|
| 48 |
+
return Constraint(specifier, hashes, links)
|
| 49 |
+
|
| 50 |
+
def is_satisfied_by(self, candidate: "Candidate") -> bool:
|
| 51 |
+
# Reject if there are any mismatched URL constraints on this package.
|
| 52 |
+
if self.links and not all(_match_link(link, candidate) for link in self.links):
|
| 53 |
+
return False
|
| 54 |
+
# We can safely always allow prereleases here since PackageFinder
|
| 55 |
+
# already implements the prerelease logic, and would have filtered out
|
| 56 |
+
# prerelease candidates if the user does not expect them.
|
| 57 |
+
return self.specifier.contains(candidate.version, prereleases=True)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class Requirement:
|
| 61 |
+
@property
|
| 62 |
+
def project_name(self) -> NormalizedName:
|
| 63 |
+
"""The "project name" of a requirement.
|
| 64 |
+
|
| 65 |
+
This is different from ``name`` if this requirement contains extras,
|
| 66 |
+
in which case ``name`` would contain the ``[...]`` part, while this
|
| 67 |
+
refers to the name of the project.
|
| 68 |
+
"""
|
| 69 |
+
raise NotImplementedError("Subclass should override")
|
| 70 |
+
|
| 71 |
+
@property
|
| 72 |
+
def name(self) -> str:
|
| 73 |
+
"""The name identifying this requirement in the resolver.
|
| 74 |
+
|
| 75 |
+
This is different from ``project_name`` if this requirement contains
|
| 76 |
+
extras, where ``project_name`` would not contain the ``[...]`` part.
|
| 77 |
+
"""
|
| 78 |
+
raise NotImplementedError("Subclass should override")
|
| 79 |
+
|
| 80 |
+
def is_satisfied_by(self, candidate: "Candidate") -> bool:
|
| 81 |
+
return False
|
| 82 |
+
|
| 83 |
+
def get_candidate_lookup(self) -> CandidateLookup:
|
| 84 |
+
raise NotImplementedError("Subclass should override")
|
| 85 |
+
|
| 86 |
+
def format_for_error(self) -> str:
|
| 87 |
+
raise NotImplementedError("Subclass should override")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _match_link(link: Link, candidate: "Candidate") -> bool:
|
| 91 |
+
if candidate.source_link:
|
| 92 |
+
return links_equivalent(link, candidate.source_link)
|
| 93 |
+
return False
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class Candidate:
|
| 97 |
+
@property
|
| 98 |
+
def project_name(self) -> NormalizedName:
|
| 99 |
+
"""The "project name" of the candidate.
|
| 100 |
+
|
| 101 |
+
This is different from ``name`` if this candidate contains extras,
|
| 102 |
+
in which case ``name`` would contain the ``[...]`` part, while this
|
| 103 |
+
refers to the name of the project.
|
| 104 |
+
"""
|
| 105 |
+
raise NotImplementedError("Override in subclass")
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def name(self) -> str:
|
| 109 |
+
"""The name identifying this candidate in the resolver.
|
| 110 |
+
|
| 111 |
+
This is different from ``project_name`` if this candidate contains
|
| 112 |
+
extras, where ``project_name`` would not contain the ``[...]`` part.
|
| 113 |
+
"""
|
| 114 |
+
raise NotImplementedError("Override in subclass")
|
| 115 |
+
|
| 116 |
+
@property
|
| 117 |
+
def version(self) -> Version:
|
| 118 |
+
raise NotImplementedError("Override in subclass")
|
| 119 |
+
|
| 120 |
+
@property
|
| 121 |
+
def is_installed(self) -> bool:
|
| 122 |
+
raise NotImplementedError("Override in subclass")
|
| 123 |
+
|
| 124 |
+
@property
|
| 125 |
+
def is_editable(self) -> bool:
|
| 126 |
+
raise NotImplementedError("Override in subclass")
|
| 127 |
+
|
| 128 |
+
@property
|
| 129 |
+
def source_link(self) -> Optional[Link]:
|
| 130 |
+
raise NotImplementedError("Override in subclass")
|
| 131 |
+
|
| 132 |
+
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
|
| 133 |
+
raise NotImplementedError("Override in subclass")
|
| 134 |
+
|
| 135 |
+
def get_install_requirement(self) -> Optional[InstallRequirement]:
|
| 136 |
+
raise NotImplementedError("Override in subclass")
|
| 137 |
+
|
| 138 |
+
def format_for_error(self) -> str:
|
| 139 |
+
raise NotImplementedError("Subclass should override")
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/candidates.py
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import sys
|
| 3 |
+
from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
|
| 4 |
+
|
| 5 |
+
from pip._vendor.packaging.requirements import InvalidRequirement
|
| 6 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 7 |
+
from pip._vendor.packaging.version import Version
|
| 8 |
+
|
| 9 |
+
from pip._internal.exceptions import (
|
| 10 |
+
HashError,
|
| 11 |
+
InstallationSubprocessError,
|
| 12 |
+
InvalidInstalledPackage,
|
| 13 |
+
MetadataInconsistent,
|
| 14 |
+
MetadataInvalid,
|
| 15 |
+
)
|
| 16 |
+
from pip._internal.metadata import BaseDistribution
|
| 17 |
+
from pip._internal.models.link import Link, links_equivalent
|
| 18 |
+
from pip._internal.models.wheel import Wheel
|
| 19 |
+
from pip._internal.req.constructors import (
|
| 20 |
+
install_req_from_editable,
|
| 21 |
+
install_req_from_line,
|
| 22 |
+
)
|
| 23 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 24 |
+
from pip._internal.utils.direct_url_helpers import direct_url_from_link
|
| 25 |
+
from pip._internal.utils.misc import normalize_version_info
|
| 26 |
+
|
| 27 |
+
from .base import Candidate, Requirement, format_name
|
| 28 |
+
|
| 29 |
+
if TYPE_CHECKING:
|
| 30 |
+
from .factory import Factory
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
BaseCandidate = Union[
|
| 35 |
+
"AlreadyInstalledCandidate",
|
| 36 |
+
"EditableCandidate",
|
| 37 |
+
"LinkCandidate",
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
# Avoid conflicting with the PyPI package "Python".
|
| 41 |
+
REQUIRES_PYTHON_IDENTIFIER = cast(NormalizedName, "<Python from Requires-Python>")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]:
|
| 45 |
+
"""The runtime version of BaseCandidate."""
|
| 46 |
+
base_candidate_classes = (
|
| 47 |
+
AlreadyInstalledCandidate,
|
| 48 |
+
EditableCandidate,
|
| 49 |
+
LinkCandidate,
|
| 50 |
+
)
|
| 51 |
+
if isinstance(candidate, base_candidate_classes):
|
| 52 |
+
return candidate
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def make_install_req_from_link(
|
| 57 |
+
link: Link, template: InstallRequirement
|
| 58 |
+
) -> InstallRequirement:
|
| 59 |
+
assert not template.editable, "template is editable"
|
| 60 |
+
if template.req:
|
| 61 |
+
line = str(template.req)
|
| 62 |
+
else:
|
| 63 |
+
line = link.url
|
| 64 |
+
ireq = install_req_from_line(
|
| 65 |
+
line,
|
| 66 |
+
user_supplied=template.user_supplied,
|
| 67 |
+
comes_from=template.comes_from,
|
| 68 |
+
use_pep517=template.use_pep517,
|
| 69 |
+
isolated=template.isolated,
|
| 70 |
+
constraint=template.constraint,
|
| 71 |
+
global_options=template.global_options,
|
| 72 |
+
hash_options=template.hash_options,
|
| 73 |
+
config_settings=template.config_settings,
|
| 74 |
+
)
|
| 75 |
+
ireq.original_link = template.original_link
|
| 76 |
+
ireq.link = link
|
| 77 |
+
ireq.extras = template.extras
|
| 78 |
+
return ireq
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def make_install_req_from_editable(
|
| 82 |
+
link: Link, template: InstallRequirement
|
| 83 |
+
) -> InstallRequirement:
|
| 84 |
+
assert template.editable, "template not editable"
|
| 85 |
+
ireq = install_req_from_editable(
|
| 86 |
+
link.url,
|
| 87 |
+
user_supplied=template.user_supplied,
|
| 88 |
+
comes_from=template.comes_from,
|
| 89 |
+
use_pep517=template.use_pep517,
|
| 90 |
+
isolated=template.isolated,
|
| 91 |
+
constraint=template.constraint,
|
| 92 |
+
permit_editable_wheels=template.permit_editable_wheels,
|
| 93 |
+
global_options=template.global_options,
|
| 94 |
+
hash_options=template.hash_options,
|
| 95 |
+
config_settings=template.config_settings,
|
| 96 |
+
)
|
| 97 |
+
ireq.extras = template.extras
|
| 98 |
+
return ireq
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _make_install_req_from_dist(
|
| 102 |
+
dist: BaseDistribution, template: InstallRequirement
|
| 103 |
+
) -> InstallRequirement:
|
| 104 |
+
if template.req:
|
| 105 |
+
line = str(template.req)
|
| 106 |
+
elif template.link:
|
| 107 |
+
line = f"{dist.canonical_name} @ {template.link.url}"
|
| 108 |
+
else:
|
| 109 |
+
line = f"{dist.canonical_name}=={dist.version}"
|
| 110 |
+
ireq = install_req_from_line(
|
| 111 |
+
line,
|
| 112 |
+
user_supplied=template.user_supplied,
|
| 113 |
+
comes_from=template.comes_from,
|
| 114 |
+
use_pep517=template.use_pep517,
|
| 115 |
+
isolated=template.isolated,
|
| 116 |
+
constraint=template.constraint,
|
| 117 |
+
global_options=template.global_options,
|
| 118 |
+
hash_options=template.hash_options,
|
| 119 |
+
config_settings=template.config_settings,
|
| 120 |
+
)
|
| 121 |
+
ireq.satisfied_by = dist
|
| 122 |
+
return ireq
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class _InstallRequirementBackedCandidate(Candidate):
|
| 126 |
+
"""A candidate backed by an ``InstallRequirement``.
|
| 127 |
+
|
| 128 |
+
This represents a package request with the target not being already
|
| 129 |
+
in the environment, and needs to be fetched and installed. The backing
|
| 130 |
+
``InstallRequirement`` is responsible for most of the leg work; this
|
| 131 |
+
class exposes appropriate information to the resolver.
|
| 132 |
+
|
| 133 |
+
:param link: The link passed to the ``InstallRequirement``. The backing
|
| 134 |
+
``InstallRequirement`` will use this link to fetch the distribution.
|
| 135 |
+
:param source_link: The link this candidate "originates" from. This is
|
| 136 |
+
different from ``link`` when the link is found in the wheel cache.
|
| 137 |
+
``link`` would point to the wheel cache, while this points to the
|
| 138 |
+
found remote link (e.g. from pypi.org).
|
| 139 |
+
"""
|
| 140 |
+
|
| 141 |
+
dist: BaseDistribution
|
| 142 |
+
is_installed = False
|
| 143 |
+
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
link: Link,
|
| 147 |
+
source_link: Link,
|
| 148 |
+
ireq: InstallRequirement,
|
| 149 |
+
factory: "Factory",
|
| 150 |
+
name: Optional[NormalizedName] = None,
|
| 151 |
+
version: Optional[Version] = None,
|
| 152 |
+
) -> None:
|
| 153 |
+
self._link = link
|
| 154 |
+
self._source_link = source_link
|
| 155 |
+
self._factory = factory
|
| 156 |
+
self._ireq = ireq
|
| 157 |
+
self._name = name
|
| 158 |
+
self._version = version
|
| 159 |
+
self.dist = self._prepare()
|
| 160 |
+
self._hash: Optional[int] = None
|
| 161 |
+
|
| 162 |
+
def __str__(self) -> str:
|
| 163 |
+
return f"{self.name} {self.version}"
|
| 164 |
+
|
| 165 |
+
def __repr__(self) -> str:
|
| 166 |
+
return f"{self.__class__.__name__}({str(self._link)!r})"
|
| 167 |
+
|
| 168 |
+
def __hash__(self) -> int:
|
| 169 |
+
if self._hash is not None:
|
| 170 |
+
return self._hash
|
| 171 |
+
|
| 172 |
+
self._hash = hash((self.__class__, self._link))
|
| 173 |
+
return self._hash
|
| 174 |
+
|
| 175 |
+
def __eq__(self, other: Any) -> bool:
|
| 176 |
+
if isinstance(other, self.__class__):
|
| 177 |
+
return links_equivalent(self._link, other._link)
|
| 178 |
+
return False
|
| 179 |
+
|
| 180 |
+
@property
|
| 181 |
+
def source_link(self) -> Optional[Link]:
|
| 182 |
+
return self._source_link
|
| 183 |
+
|
| 184 |
+
@property
|
| 185 |
+
def project_name(self) -> NormalizedName:
|
| 186 |
+
"""The normalised name of the project the candidate refers to"""
|
| 187 |
+
if self._name is None:
|
| 188 |
+
self._name = self.dist.canonical_name
|
| 189 |
+
return self._name
|
| 190 |
+
|
| 191 |
+
@property
|
| 192 |
+
def name(self) -> str:
|
| 193 |
+
return self.project_name
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def version(self) -> Version:
|
| 197 |
+
if self._version is None:
|
| 198 |
+
self._version = self.dist.version
|
| 199 |
+
return self._version
|
| 200 |
+
|
| 201 |
+
def format_for_error(self) -> str:
|
| 202 |
+
return (
|
| 203 |
+
f"{self.name} {self.version} "
|
| 204 |
+
f"(from {self._link.file_path if self._link.is_file else self._link})"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
def _prepare_distribution(self) -> BaseDistribution:
|
| 208 |
+
raise NotImplementedError("Override in subclass")
|
| 209 |
+
|
| 210 |
+
def _check_metadata_consistency(self, dist: BaseDistribution) -> None:
|
| 211 |
+
"""Check for consistency of project name and version of dist."""
|
| 212 |
+
if self._name is not None and self._name != dist.canonical_name:
|
| 213 |
+
raise MetadataInconsistent(
|
| 214 |
+
self._ireq,
|
| 215 |
+
"name",
|
| 216 |
+
self._name,
|
| 217 |
+
dist.canonical_name,
|
| 218 |
+
)
|
| 219 |
+
if self._version is not None and self._version != dist.version:
|
| 220 |
+
raise MetadataInconsistent(
|
| 221 |
+
self._ireq,
|
| 222 |
+
"version",
|
| 223 |
+
str(self._version),
|
| 224 |
+
str(dist.version),
|
| 225 |
+
)
|
| 226 |
+
# check dependencies are valid
|
| 227 |
+
# TODO performance: this means we iterate the dependencies at least twice,
|
| 228 |
+
# we may want to cache parsed Requires-Dist
|
| 229 |
+
try:
|
| 230 |
+
list(dist.iter_dependencies(list(dist.iter_provided_extras())))
|
| 231 |
+
except InvalidRequirement as e:
|
| 232 |
+
raise MetadataInvalid(self._ireq, str(e))
|
| 233 |
+
|
| 234 |
+
def _prepare(self) -> BaseDistribution:
|
| 235 |
+
try:
|
| 236 |
+
dist = self._prepare_distribution()
|
| 237 |
+
except HashError as e:
|
| 238 |
+
# Provide HashError the underlying ireq that caused it. This
|
| 239 |
+
# provides context for the resulting error message to show the
|
| 240 |
+
# offending line to the user.
|
| 241 |
+
e.req = self._ireq
|
| 242 |
+
raise
|
| 243 |
+
except InstallationSubprocessError as exc:
|
| 244 |
+
# The output has been presented already, so don't duplicate it.
|
| 245 |
+
exc.context = "See above for output."
|
| 246 |
+
raise
|
| 247 |
+
|
| 248 |
+
self._check_metadata_consistency(dist)
|
| 249 |
+
return dist
|
| 250 |
+
|
| 251 |
+
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
|
| 252 |
+
# Emit the Requires-Python requirement first to fail fast on
|
| 253 |
+
# unsupported candidates and avoid pointless downloads/preparation.
|
| 254 |
+
yield self._factory.make_requires_python_requirement(self.dist.requires_python)
|
| 255 |
+
requires = self.dist.iter_dependencies() if with_requires else ()
|
| 256 |
+
for r in requires:
|
| 257 |
+
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
|
| 258 |
+
|
| 259 |
+
def get_install_requirement(self) -> Optional[InstallRequirement]:
|
| 260 |
+
return self._ireq
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class LinkCandidate(_InstallRequirementBackedCandidate):
|
| 264 |
+
is_editable = False
|
| 265 |
+
|
| 266 |
+
def __init__(
|
| 267 |
+
self,
|
| 268 |
+
link: Link,
|
| 269 |
+
template: InstallRequirement,
|
| 270 |
+
factory: "Factory",
|
| 271 |
+
name: Optional[NormalizedName] = None,
|
| 272 |
+
version: Optional[Version] = None,
|
| 273 |
+
) -> None:
|
| 274 |
+
source_link = link
|
| 275 |
+
cache_entry = factory.get_wheel_cache_entry(source_link, name)
|
| 276 |
+
if cache_entry is not None:
|
| 277 |
+
logger.debug("Using cached wheel link: %s", cache_entry.link)
|
| 278 |
+
link = cache_entry.link
|
| 279 |
+
ireq = make_install_req_from_link(link, template)
|
| 280 |
+
assert ireq.link == link
|
| 281 |
+
if ireq.link.is_wheel and not ireq.link.is_file:
|
| 282 |
+
wheel = Wheel(ireq.link.filename)
|
| 283 |
+
wheel_name = canonicalize_name(wheel.name)
|
| 284 |
+
assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel"
|
| 285 |
+
# Version may not be present for PEP 508 direct URLs
|
| 286 |
+
if version is not None:
|
| 287 |
+
wheel_version = Version(wheel.version)
|
| 288 |
+
assert (
|
| 289 |
+
version == wheel_version
|
| 290 |
+
), f"{version!r} != {wheel_version!r} for wheel {name}"
|
| 291 |
+
|
| 292 |
+
if cache_entry is not None:
|
| 293 |
+
assert ireq.link.is_wheel
|
| 294 |
+
assert ireq.link.is_file
|
| 295 |
+
if cache_entry.persistent and template.link is template.original_link:
|
| 296 |
+
ireq.cached_wheel_source_link = source_link
|
| 297 |
+
if cache_entry.origin is not None:
|
| 298 |
+
ireq.download_info = cache_entry.origin
|
| 299 |
+
else:
|
| 300 |
+
# Legacy cache entry that does not have origin.json.
|
| 301 |
+
# download_info may miss the archive_info.hashes field.
|
| 302 |
+
ireq.download_info = direct_url_from_link(
|
| 303 |
+
source_link, link_is_in_wheel_cache=cache_entry.persistent
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
super().__init__(
|
| 307 |
+
link=link,
|
| 308 |
+
source_link=source_link,
|
| 309 |
+
ireq=ireq,
|
| 310 |
+
factory=factory,
|
| 311 |
+
name=name,
|
| 312 |
+
version=version,
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
def _prepare_distribution(self) -> BaseDistribution:
|
| 316 |
+
preparer = self._factory.preparer
|
| 317 |
+
return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class EditableCandidate(_InstallRequirementBackedCandidate):
|
| 321 |
+
is_editable = True
|
| 322 |
+
|
| 323 |
+
def __init__(
|
| 324 |
+
self,
|
| 325 |
+
link: Link,
|
| 326 |
+
template: InstallRequirement,
|
| 327 |
+
factory: "Factory",
|
| 328 |
+
name: Optional[NormalizedName] = None,
|
| 329 |
+
version: Optional[Version] = None,
|
| 330 |
+
) -> None:
|
| 331 |
+
super().__init__(
|
| 332 |
+
link=link,
|
| 333 |
+
source_link=link,
|
| 334 |
+
ireq=make_install_req_from_editable(link, template),
|
| 335 |
+
factory=factory,
|
| 336 |
+
name=name,
|
| 337 |
+
version=version,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
def _prepare_distribution(self) -> BaseDistribution:
|
| 341 |
+
return self._factory.preparer.prepare_editable_requirement(self._ireq)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
class AlreadyInstalledCandidate(Candidate):
|
| 345 |
+
is_installed = True
|
| 346 |
+
source_link = None
|
| 347 |
+
|
| 348 |
+
def __init__(
|
| 349 |
+
self,
|
| 350 |
+
dist: BaseDistribution,
|
| 351 |
+
template: InstallRequirement,
|
| 352 |
+
factory: "Factory",
|
| 353 |
+
) -> None:
|
| 354 |
+
self.dist = dist
|
| 355 |
+
self._ireq = _make_install_req_from_dist(dist, template)
|
| 356 |
+
self._factory = factory
|
| 357 |
+
self._version = None
|
| 358 |
+
|
| 359 |
+
# This is just logging some messages, so we can do it eagerly.
|
| 360 |
+
# The returned dist would be exactly the same as self.dist because we
|
| 361 |
+
# set satisfied_by in _make_install_req_from_dist.
|
| 362 |
+
# TODO: Supply reason based on force_reinstall and upgrade_strategy.
|
| 363 |
+
skip_reason = "already satisfied"
|
| 364 |
+
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
|
| 365 |
+
|
| 366 |
+
def __str__(self) -> str:
|
| 367 |
+
return str(self.dist)
|
| 368 |
+
|
| 369 |
+
def __repr__(self) -> str:
|
| 370 |
+
return f"{self.__class__.__name__}({self.dist!r})"
|
| 371 |
+
|
| 372 |
+
def __eq__(self, other: object) -> bool:
|
| 373 |
+
if not isinstance(other, AlreadyInstalledCandidate):
|
| 374 |
+
return NotImplemented
|
| 375 |
+
return self.name == other.name and self.version == other.version
|
| 376 |
+
|
| 377 |
+
def __hash__(self) -> int:
|
| 378 |
+
return hash((self.name, self.version))
|
| 379 |
+
|
| 380 |
+
@property
|
| 381 |
+
def project_name(self) -> NormalizedName:
|
| 382 |
+
return self.dist.canonical_name
|
| 383 |
+
|
| 384 |
+
@property
|
| 385 |
+
def name(self) -> str:
|
| 386 |
+
return self.project_name
|
| 387 |
+
|
| 388 |
+
@property
|
| 389 |
+
def version(self) -> Version:
|
| 390 |
+
if self._version is None:
|
| 391 |
+
self._version = self.dist.version
|
| 392 |
+
return self._version
|
| 393 |
+
|
| 394 |
+
@property
|
| 395 |
+
def is_editable(self) -> bool:
|
| 396 |
+
return self.dist.editable
|
| 397 |
+
|
| 398 |
+
def format_for_error(self) -> str:
|
| 399 |
+
return f"{self.name} {self.version} (Installed)"
|
| 400 |
+
|
| 401 |
+
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
|
| 402 |
+
if not with_requires:
|
| 403 |
+
return
|
| 404 |
+
|
| 405 |
+
try:
|
| 406 |
+
for r in self.dist.iter_dependencies():
|
| 407 |
+
yield from self._factory.make_requirements_from_spec(str(r), self._ireq)
|
| 408 |
+
except InvalidRequirement as exc:
|
| 409 |
+
raise InvalidInstalledPackage(dist=self.dist, invalid_exc=exc) from None
|
| 410 |
+
|
| 411 |
+
def get_install_requirement(self) -> Optional[InstallRequirement]:
|
| 412 |
+
return None
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
class ExtrasCandidate(Candidate):
|
| 416 |
+
"""A candidate that has 'extras', indicating additional dependencies.
|
| 417 |
+
|
| 418 |
+
Requirements can be for a project with dependencies, something like
|
| 419 |
+
foo[extra]. The extras don't affect the project/version being installed
|
| 420 |
+
directly, but indicate that we need additional dependencies. We model that
|
| 421 |
+
by having an artificial ExtrasCandidate that wraps the "base" candidate.
|
| 422 |
+
|
| 423 |
+
The ExtrasCandidate differs from the base in the following ways:
|
| 424 |
+
|
| 425 |
+
1. It has a unique name, of the form foo[extra]. This causes the resolver
|
| 426 |
+
to treat it as a separate node in the dependency graph.
|
| 427 |
+
2. When we're getting the candidate's dependencies,
|
| 428 |
+
a) We specify that we want the extra dependencies as well.
|
| 429 |
+
b) We add a dependency on the base candidate.
|
| 430 |
+
See below for why this is needed.
|
| 431 |
+
3. We return None for the underlying InstallRequirement, as the base
|
| 432 |
+
candidate will provide it, and we don't want to end up with duplicates.
|
| 433 |
+
|
| 434 |
+
The dependency on the base candidate is needed so that the resolver can't
|
| 435 |
+
decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
|
| 436 |
+
version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
|
| 437 |
+
respectively forces the resolver to recognise that this is a conflict.
|
| 438 |
+
"""
|
| 439 |
+
|
| 440 |
+
def __init__(
|
| 441 |
+
self,
|
| 442 |
+
base: BaseCandidate,
|
| 443 |
+
extras: FrozenSet[str],
|
| 444 |
+
*,
|
| 445 |
+
comes_from: Optional[InstallRequirement] = None,
|
| 446 |
+
) -> None:
|
| 447 |
+
"""
|
| 448 |
+
:param comes_from: the InstallRequirement that led to this candidate if it
|
| 449 |
+
differs from the base's InstallRequirement. This will often be the
|
| 450 |
+
case in the sense that this candidate's requirement has the extras
|
| 451 |
+
while the base's does not. Unlike the InstallRequirement backed
|
| 452 |
+
candidates, this requirement is used solely for reporting purposes,
|
| 453 |
+
it does not do any leg work.
|
| 454 |
+
"""
|
| 455 |
+
self.base = base
|
| 456 |
+
self.extras = frozenset(canonicalize_name(e) for e in extras)
|
| 457 |
+
self._comes_from = comes_from if comes_from is not None else self.base._ireq
|
| 458 |
+
|
| 459 |
+
def __str__(self) -> str:
|
| 460 |
+
name, rest = str(self.base).split(" ", 1)
|
| 461 |
+
return "{}[{}] {}".format(name, ",".join(self.extras), rest)
|
| 462 |
+
|
| 463 |
+
def __repr__(self) -> str:
|
| 464 |
+
return f"{self.__class__.__name__}(base={self.base!r}, extras={self.extras!r})"
|
| 465 |
+
|
| 466 |
+
def __hash__(self) -> int:
|
| 467 |
+
return hash((self.base, self.extras))
|
| 468 |
+
|
| 469 |
+
def __eq__(self, other: Any) -> bool:
|
| 470 |
+
if isinstance(other, self.__class__):
|
| 471 |
+
return self.base == other.base and self.extras == other.extras
|
| 472 |
+
return False
|
| 473 |
+
|
| 474 |
+
@property
|
| 475 |
+
def project_name(self) -> NormalizedName:
|
| 476 |
+
return self.base.project_name
|
| 477 |
+
|
| 478 |
+
@property
|
| 479 |
+
def name(self) -> str:
|
| 480 |
+
"""The normalised name of the project the candidate refers to"""
|
| 481 |
+
return format_name(self.base.project_name, self.extras)
|
| 482 |
+
|
| 483 |
+
@property
|
| 484 |
+
def version(self) -> Version:
|
| 485 |
+
return self.base.version
|
| 486 |
+
|
| 487 |
+
def format_for_error(self) -> str:
|
| 488 |
+
return "{} [{}]".format(
|
| 489 |
+
self.base.format_for_error(), ", ".join(sorted(self.extras))
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
@property
|
| 493 |
+
def is_installed(self) -> bool:
|
| 494 |
+
return self.base.is_installed
|
| 495 |
+
|
| 496 |
+
@property
|
| 497 |
+
def is_editable(self) -> bool:
|
| 498 |
+
return self.base.is_editable
|
| 499 |
+
|
| 500 |
+
@property
|
| 501 |
+
def source_link(self) -> Optional[Link]:
|
| 502 |
+
return self.base.source_link
|
| 503 |
+
|
| 504 |
+
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
|
| 505 |
+
factory = self.base._factory
|
| 506 |
+
|
| 507 |
+
# Add a dependency on the exact base
|
| 508 |
+
# (See note 2b in the class docstring)
|
| 509 |
+
yield factory.make_requirement_from_candidate(self.base)
|
| 510 |
+
if not with_requires:
|
| 511 |
+
return
|
| 512 |
+
|
| 513 |
+
# The user may have specified extras that the candidate doesn't
|
| 514 |
+
# support. We ignore any unsupported extras here.
|
| 515 |
+
valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras())
|
| 516 |
+
invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras())
|
| 517 |
+
for extra in sorted(invalid_extras):
|
| 518 |
+
logger.warning(
|
| 519 |
+
"%s %s does not provide the extra '%s'",
|
| 520 |
+
self.base.name,
|
| 521 |
+
self.version,
|
| 522 |
+
extra,
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
for r in self.base.dist.iter_dependencies(valid_extras):
|
| 526 |
+
yield from factory.make_requirements_from_spec(
|
| 527 |
+
str(r),
|
| 528 |
+
self._comes_from,
|
| 529 |
+
valid_extras,
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
def get_install_requirement(self) -> Optional[InstallRequirement]:
|
| 533 |
+
# We don't return anything here, because we always
|
| 534 |
+
# depend on the base candidate, and we'll get the
|
| 535 |
+
# install requirement from that.
|
| 536 |
+
return None
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
class RequiresPythonCandidate(Candidate):
|
| 540 |
+
is_installed = False
|
| 541 |
+
source_link = None
|
| 542 |
+
|
| 543 |
+
def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
|
| 544 |
+
if py_version_info is not None:
|
| 545 |
+
version_info = normalize_version_info(py_version_info)
|
| 546 |
+
else:
|
| 547 |
+
version_info = sys.version_info[:3]
|
| 548 |
+
self._version = Version(".".join(str(c) for c in version_info))
|
| 549 |
+
|
| 550 |
+
# We don't need to implement __eq__() and __ne__() since there is always
|
| 551 |
+
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
|
| 552 |
+
# The built-in object.__eq__() and object.__ne__() do exactly what we want.
|
| 553 |
+
|
| 554 |
+
def __str__(self) -> str:
|
| 555 |
+
return f"Python {self._version}"
|
| 556 |
+
|
| 557 |
+
def __repr__(self) -> str:
|
| 558 |
+
return f"{self.__class__.__name__}({self._version!r})"
|
| 559 |
+
|
| 560 |
+
@property
|
| 561 |
+
def project_name(self) -> NormalizedName:
|
| 562 |
+
return REQUIRES_PYTHON_IDENTIFIER
|
| 563 |
+
|
| 564 |
+
@property
|
| 565 |
+
def name(self) -> str:
|
| 566 |
+
return REQUIRES_PYTHON_IDENTIFIER
|
| 567 |
+
|
| 568 |
+
@property
|
| 569 |
+
def version(self) -> Version:
|
| 570 |
+
return self._version
|
| 571 |
+
|
| 572 |
+
def format_for_error(self) -> str:
|
| 573 |
+
return f"Python {self.version}"
|
| 574 |
+
|
| 575 |
+
def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
|
| 576 |
+
return ()
|
| 577 |
+
|
| 578 |
+
def get_install_requirement(self) -> Optional[InstallRequirement]:
|
| 579 |
+
return None
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/factory.py
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import functools
|
| 3 |
+
import logging
|
| 4 |
+
from typing import (
|
| 5 |
+
TYPE_CHECKING,
|
| 6 |
+
Callable,
|
| 7 |
+
Dict,
|
| 8 |
+
FrozenSet,
|
| 9 |
+
Iterable,
|
| 10 |
+
Iterator,
|
| 11 |
+
List,
|
| 12 |
+
Mapping,
|
| 13 |
+
NamedTuple,
|
| 14 |
+
Optional,
|
| 15 |
+
Protocol,
|
| 16 |
+
Sequence,
|
| 17 |
+
Set,
|
| 18 |
+
Tuple,
|
| 19 |
+
TypeVar,
|
| 20 |
+
cast,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
from pip._vendor.packaging.requirements import InvalidRequirement
|
| 24 |
+
from pip._vendor.packaging.specifiers import SpecifierSet
|
| 25 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 26 |
+
from pip._vendor.packaging.version import InvalidVersion, Version
|
| 27 |
+
from pip._vendor.resolvelib import ResolutionImpossible
|
| 28 |
+
|
| 29 |
+
from pip._internal.cache import CacheEntry, WheelCache
|
| 30 |
+
from pip._internal.exceptions import (
|
| 31 |
+
DistributionNotFound,
|
| 32 |
+
InstallationError,
|
| 33 |
+
InvalidInstalledPackage,
|
| 34 |
+
MetadataInconsistent,
|
| 35 |
+
MetadataInvalid,
|
| 36 |
+
UnsupportedPythonVersion,
|
| 37 |
+
UnsupportedWheel,
|
| 38 |
+
)
|
| 39 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 40 |
+
from pip._internal.metadata import BaseDistribution, get_default_environment
|
| 41 |
+
from pip._internal.models.link import Link
|
| 42 |
+
from pip._internal.models.wheel import Wheel
|
| 43 |
+
from pip._internal.operations.prepare import RequirementPreparer
|
| 44 |
+
from pip._internal.req.constructors import (
|
| 45 |
+
install_req_drop_extras,
|
| 46 |
+
install_req_from_link_and_ireq,
|
| 47 |
+
)
|
| 48 |
+
from pip._internal.req.req_install import (
|
| 49 |
+
InstallRequirement,
|
| 50 |
+
check_invalid_constraint_type,
|
| 51 |
+
)
|
| 52 |
+
from pip._internal.resolution.base import InstallRequirementProvider
|
| 53 |
+
from pip._internal.utils.compatibility_tags import get_supported
|
| 54 |
+
from pip._internal.utils.hashes import Hashes
|
| 55 |
+
from pip._internal.utils.packaging import get_requirement
|
| 56 |
+
from pip._internal.utils.virtualenv import running_under_virtualenv
|
| 57 |
+
|
| 58 |
+
from .base import Candidate, Constraint, Requirement
|
| 59 |
+
from .candidates import (
|
| 60 |
+
AlreadyInstalledCandidate,
|
| 61 |
+
BaseCandidate,
|
| 62 |
+
EditableCandidate,
|
| 63 |
+
ExtrasCandidate,
|
| 64 |
+
LinkCandidate,
|
| 65 |
+
RequiresPythonCandidate,
|
| 66 |
+
as_base_candidate,
|
| 67 |
+
)
|
| 68 |
+
from .found_candidates import FoundCandidates, IndexCandidateInfo
|
| 69 |
+
from .requirements import (
|
| 70 |
+
ExplicitRequirement,
|
| 71 |
+
RequiresPythonRequirement,
|
| 72 |
+
SpecifierRequirement,
|
| 73 |
+
SpecifierWithoutExtrasRequirement,
|
| 74 |
+
UnsatisfiableRequirement,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
if TYPE_CHECKING:
|
| 78 |
+
|
| 79 |
+
class ConflictCause(Protocol):
|
| 80 |
+
requirement: RequiresPythonRequirement
|
| 81 |
+
parent: Candidate
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
logger = logging.getLogger(__name__)
|
| 85 |
+
|
| 86 |
+
C = TypeVar("C")
|
| 87 |
+
Cache = Dict[Link, C]
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class CollectedRootRequirements(NamedTuple):
|
| 91 |
+
requirements: List[Requirement]
|
| 92 |
+
constraints: Dict[str, Constraint]
|
| 93 |
+
user_requested: Dict[str, int]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class Factory:
|
| 97 |
+
def __init__(
|
| 98 |
+
self,
|
| 99 |
+
finder: PackageFinder,
|
| 100 |
+
preparer: RequirementPreparer,
|
| 101 |
+
make_install_req: InstallRequirementProvider,
|
| 102 |
+
wheel_cache: Optional[WheelCache],
|
| 103 |
+
use_user_site: bool,
|
| 104 |
+
force_reinstall: bool,
|
| 105 |
+
ignore_installed: bool,
|
| 106 |
+
ignore_requires_python: bool,
|
| 107 |
+
py_version_info: Optional[Tuple[int, ...]] = None,
|
| 108 |
+
) -> None:
|
| 109 |
+
self._finder = finder
|
| 110 |
+
self.preparer = preparer
|
| 111 |
+
self._wheel_cache = wheel_cache
|
| 112 |
+
self._python_candidate = RequiresPythonCandidate(py_version_info)
|
| 113 |
+
self._make_install_req_from_spec = make_install_req
|
| 114 |
+
self._use_user_site = use_user_site
|
| 115 |
+
self._force_reinstall = force_reinstall
|
| 116 |
+
self._ignore_requires_python = ignore_requires_python
|
| 117 |
+
|
| 118 |
+
self._build_failures: Cache[InstallationError] = {}
|
| 119 |
+
self._link_candidate_cache: Cache[LinkCandidate] = {}
|
| 120 |
+
self._editable_candidate_cache: Cache[EditableCandidate] = {}
|
| 121 |
+
self._installed_candidate_cache: Dict[str, AlreadyInstalledCandidate] = {}
|
| 122 |
+
self._extras_candidate_cache: Dict[
|
| 123 |
+
Tuple[int, FrozenSet[NormalizedName]], ExtrasCandidate
|
| 124 |
+
] = {}
|
| 125 |
+
self._supported_tags_cache = get_supported()
|
| 126 |
+
|
| 127 |
+
if not ignore_installed:
|
| 128 |
+
env = get_default_environment()
|
| 129 |
+
self._installed_dists = {
|
| 130 |
+
dist.canonical_name: dist
|
| 131 |
+
for dist in env.iter_installed_distributions(local_only=False)
|
| 132 |
+
}
|
| 133 |
+
else:
|
| 134 |
+
self._installed_dists = {}
|
| 135 |
+
|
| 136 |
+
@property
|
| 137 |
+
def force_reinstall(self) -> bool:
|
| 138 |
+
return self._force_reinstall
|
| 139 |
+
|
| 140 |
+
def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None:
|
| 141 |
+
if not link.is_wheel:
|
| 142 |
+
return
|
| 143 |
+
wheel = Wheel(link.filename)
|
| 144 |
+
if wheel.supported(self._finder.target_python.get_unsorted_tags()):
|
| 145 |
+
return
|
| 146 |
+
msg = f"{link.filename} is not a supported wheel on this platform."
|
| 147 |
+
raise UnsupportedWheel(msg)
|
| 148 |
+
|
| 149 |
+
def _make_extras_candidate(
|
| 150 |
+
self,
|
| 151 |
+
base: BaseCandidate,
|
| 152 |
+
extras: FrozenSet[str],
|
| 153 |
+
*,
|
| 154 |
+
comes_from: Optional[InstallRequirement] = None,
|
| 155 |
+
) -> ExtrasCandidate:
|
| 156 |
+
cache_key = (id(base), frozenset(canonicalize_name(e) for e in extras))
|
| 157 |
+
try:
|
| 158 |
+
candidate = self._extras_candidate_cache[cache_key]
|
| 159 |
+
except KeyError:
|
| 160 |
+
candidate = ExtrasCandidate(base, extras, comes_from=comes_from)
|
| 161 |
+
self._extras_candidate_cache[cache_key] = candidate
|
| 162 |
+
return candidate
|
| 163 |
+
|
| 164 |
+
def _make_candidate_from_dist(
|
| 165 |
+
self,
|
| 166 |
+
dist: BaseDistribution,
|
| 167 |
+
extras: FrozenSet[str],
|
| 168 |
+
template: InstallRequirement,
|
| 169 |
+
) -> Candidate:
|
| 170 |
+
try:
|
| 171 |
+
base = self._installed_candidate_cache[dist.canonical_name]
|
| 172 |
+
except KeyError:
|
| 173 |
+
base = AlreadyInstalledCandidate(dist, template, factory=self)
|
| 174 |
+
self._installed_candidate_cache[dist.canonical_name] = base
|
| 175 |
+
if not extras:
|
| 176 |
+
return base
|
| 177 |
+
return self._make_extras_candidate(base, extras, comes_from=template)
|
| 178 |
+
|
| 179 |
+
def _make_candidate_from_link(
|
| 180 |
+
self,
|
| 181 |
+
link: Link,
|
| 182 |
+
extras: FrozenSet[str],
|
| 183 |
+
template: InstallRequirement,
|
| 184 |
+
name: Optional[NormalizedName],
|
| 185 |
+
version: Optional[Version],
|
| 186 |
+
) -> Optional[Candidate]:
|
| 187 |
+
base: Optional[BaseCandidate] = self._make_base_candidate_from_link(
|
| 188 |
+
link, template, name, version
|
| 189 |
+
)
|
| 190 |
+
if not extras or base is None:
|
| 191 |
+
return base
|
| 192 |
+
return self._make_extras_candidate(base, extras, comes_from=template)
|
| 193 |
+
|
| 194 |
+
def _make_base_candidate_from_link(
|
| 195 |
+
self,
|
| 196 |
+
link: Link,
|
| 197 |
+
template: InstallRequirement,
|
| 198 |
+
name: Optional[NormalizedName],
|
| 199 |
+
version: Optional[Version],
|
| 200 |
+
) -> Optional[BaseCandidate]:
|
| 201 |
+
# TODO: Check already installed candidate, and use it if the link and
|
| 202 |
+
# editable flag match.
|
| 203 |
+
|
| 204 |
+
if link in self._build_failures:
|
| 205 |
+
# We already tried this candidate before, and it does not build.
|
| 206 |
+
# Don't bother trying again.
|
| 207 |
+
return None
|
| 208 |
+
|
| 209 |
+
if template.editable:
|
| 210 |
+
if link not in self._editable_candidate_cache:
|
| 211 |
+
try:
|
| 212 |
+
self._editable_candidate_cache[link] = EditableCandidate(
|
| 213 |
+
link,
|
| 214 |
+
template,
|
| 215 |
+
factory=self,
|
| 216 |
+
name=name,
|
| 217 |
+
version=version,
|
| 218 |
+
)
|
| 219 |
+
except (MetadataInconsistent, MetadataInvalid) as e:
|
| 220 |
+
logger.info(
|
| 221 |
+
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
|
| 222 |
+
link,
|
| 223 |
+
e,
|
| 224 |
+
extra={"markup": True},
|
| 225 |
+
)
|
| 226 |
+
self._build_failures[link] = e
|
| 227 |
+
return None
|
| 228 |
+
|
| 229 |
+
return self._editable_candidate_cache[link]
|
| 230 |
+
else:
|
| 231 |
+
if link not in self._link_candidate_cache:
|
| 232 |
+
try:
|
| 233 |
+
self._link_candidate_cache[link] = LinkCandidate(
|
| 234 |
+
link,
|
| 235 |
+
template,
|
| 236 |
+
factory=self,
|
| 237 |
+
name=name,
|
| 238 |
+
version=version,
|
| 239 |
+
)
|
| 240 |
+
except MetadataInconsistent as e:
|
| 241 |
+
logger.info(
|
| 242 |
+
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
|
| 243 |
+
link,
|
| 244 |
+
e,
|
| 245 |
+
extra={"markup": True},
|
| 246 |
+
)
|
| 247 |
+
self._build_failures[link] = e
|
| 248 |
+
return None
|
| 249 |
+
return self._link_candidate_cache[link]
|
| 250 |
+
|
| 251 |
+
def _iter_found_candidates(
|
| 252 |
+
self,
|
| 253 |
+
ireqs: Sequence[InstallRequirement],
|
| 254 |
+
specifier: SpecifierSet,
|
| 255 |
+
hashes: Hashes,
|
| 256 |
+
prefers_installed: bool,
|
| 257 |
+
incompatible_ids: Set[int],
|
| 258 |
+
) -> Iterable[Candidate]:
|
| 259 |
+
if not ireqs:
|
| 260 |
+
return ()
|
| 261 |
+
|
| 262 |
+
# The InstallRequirement implementation requires us to give it a
|
| 263 |
+
# "template". Here we just choose the first requirement to represent
|
| 264 |
+
# all of them.
|
| 265 |
+
# Hopefully the Project model can correct this mismatch in the future.
|
| 266 |
+
template = ireqs[0]
|
| 267 |
+
assert template.req, "Candidates found on index must be PEP 508"
|
| 268 |
+
name = canonicalize_name(template.req.name)
|
| 269 |
+
|
| 270 |
+
extras: FrozenSet[str] = frozenset()
|
| 271 |
+
for ireq in ireqs:
|
| 272 |
+
assert ireq.req, "Candidates found on index must be PEP 508"
|
| 273 |
+
specifier &= ireq.req.specifier
|
| 274 |
+
hashes &= ireq.hashes(trust_internet=False)
|
| 275 |
+
extras |= frozenset(ireq.extras)
|
| 276 |
+
|
| 277 |
+
def _get_installed_candidate() -> Optional[Candidate]:
|
| 278 |
+
"""Get the candidate for the currently-installed version."""
|
| 279 |
+
# If --force-reinstall is set, we want the version from the index
|
| 280 |
+
# instead, so we "pretend" there is nothing installed.
|
| 281 |
+
if self._force_reinstall:
|
| 282 |
+
return None
|
| 283 |
+
try:
|
| 284 |
+
installed_dist = self._installed_dists[name]
|
| 285 |
+
except KeyError:
|
| 286 |
+
return None
|
| 287 |
+
|
| 288 |
+
try:
|
| 289 |
+
# Don't use the installed distribution if its version
|
| 290 |
+
# does not fit the current dependency graph.
|
| 291 |
+
if not specifier.contains(installed_dist.version, prereleases=True):
|
| 292 |
+
return None
|
| 293 |
+
except InvalidVersion as e:
|
| 294 |
+
raise InvalidInstalledPackage(dist=installed_dist, invalid_exc=e)
|
| 295 |
+
|
| 296 |
+
candidate = self._make_candidate_from_dist(
|
| 297 |
+
dist=installed_dist,
|
| 298 |
+
extras=extras,
|
| 299 |
+
template=template,
|
| 300 |
+
)
|
| 301 |
+
# The candidate is a known incompatibility. Don't use it.
|
| 302 |
+
if id(candidate) in incompatible_ids:
|
| 303 |
+
return None
|
| 304 |
+
return candidate
|
| 305 |
+
|
| 306 |
+
def iter_index_candidate_infos() -> Iterator[IndexCandidateInfo]:
|
| 307 |
+
result = self._finder.find_best_candidate(
|
| 308 |
+
project_name=name,
|
| 309 |
+
specifier=specifier,
|
| 310 |
+
hashes=hashes,
|
| 311 |
+
)
|
| 312 |
+
icans = result.applicable_candidates
|
| 313 |
+
|
| 314 |
+
# PEP 592: Yanked releases are ignored unless the specifier
|
| 315 |
+
# explicitly pins a version (via '==' or '===') that can be
|
| 316 |
+
# solely satisfied by a yanked release.
|
| 317 |
+
all_yanked = all(ican.link.is_yanked for ican in icans)
|
| 318 |
+
|
| 319 |
+
def is_pinned(specifier: SpecifierSet) -> bool:
|
| 320 |
+
for sp in specifier:
|
| 321 |
+
if sp.operator == "===":
|
| 322 |
+
return True
|
| 323 |
+
if sp.operator != "==":
|
| 324 |
+
continue
|
| 325 |
+
if sp.version.endswith(".*"):
|
| 326 |
+
continue
|
| 327 |
+
return True
|
| 328 |
+
return False
|
| 329 |
+
|
| 330 |
+
pinned = is_pinned(specifier)
|
| 331 |
+
|
| 332 |
+
# PackageFinder returns earlier versions first, so we reverse.
|
| 333 |
+
for ican in reversed(icans):
|
| 334 |
+
if not (all_yanked and pinned) and ican.link.is_yanked:
|
| 335 |
+
continue
|
| 336 |
+
func = functools.partial(
|
| 337 |
+
self._make_candidate_from_link,
|
| 338 |
+
link=ican.link,
|
| 339 |
+
extras=extras,
|
| 340 |
+
template=template,
|
| 341 |
+
name=name,
|
| 342 |
+
version=ican.version,
|
| 343 |
+
)
|
| 344 |
+
yield ican.version, func
|
| 345 |
+
|
| 346 |
+
return FoundCandidates(
|
| 347 |
+
iter_index_candidate_infos,
|
| 348 |
+
_get_installed_candidate(),
|
| 349 |
+
prefers_installed,
|
| 350 |
+
incompatible_ids,
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
def _iter_explicit_candidates_from_base(
|
| 354 |
+
self,
|
| 355 |
+
base_requirements: Iterable[Requirement],
|
| 356 |
+
extras: FrozenSet[str],
|
| 357 |
+
) -> Iterator[Candidate]:
|
| 358 |
+
"""Produce explicit candidates from the base given an extra-ed package.
|
| 359 |
+
|
| 360 |
+
:param base_requirements: Requirements known to the resolver. The
|
| 361 |
+
requirements are guaranteed to not have extras.
|
| 362 |
+
:param extras: The extras to inject into the explicit requirements'
|
| 363 |
+
candidates.
|
| 364 |
+
"""
|
| 365 |
+
for req in base_requirements:
|
| 366 |
+
lookup_cand, _ = req.get_candidate_lookup()
|
| 367 |
+
if lookup_cand is None: # Not explicit.
|
| 368 |
+
continue
|
| 369 |
+
# We've stripped extras from the identifier, and should always
|
| 370 |
+
# get a BaseCandidate here, unless there's a bug elsewhere.
|
| 371 |
+
base_cand = as_base_candidate(lookup_cand)
|
| 372 |
+
assert base_cand is not None, "no extras here"
|
| 373 |
+
yield self._make_extras_candidate(base_cand, extras)
|
| 374 |
+
|
| 375 |
+
def _iter_candidates_from_constraints(
|
| 376 |
+
self,
|
| 377 |
+
identifier: str,
|
| 378 |
+
constraint: Constraint,
|
| 379 |
+
template: InstallRequirement,
|
| 380 |
+
) -> Iterator[Candidate]:
|
| 381 |
+
"""Produce explicit candidates from constraints.
|
| 382 |
+
|
| 383 |
+
This creates "fake" InstallRequirement objects that are basically clones
|
| 384 |
+
of what "should" be the template, but with original_link set to link.
|
| 385 |
+
"""
|
| 386 |
+
for link in constraint.links:
|
| 387 |
+
self._fail_if_link_is_unsupported_wheel(link)
|
| 388 |
+
candidate = self._make_base_candidate_from_link(
|
| 389 |
+
link,
|
| 390 |
+
template=install_req_from_link_and_ireq(link, template),
|
| 391 |
+
name=canonicalize_name(identifier),
|
| 392 |
+
version=None,
|
| 393 |
+
)
|
| 394 |
+
if candidate:
|
| 395 |
+
yield candidate
|
| 396 |
+
|
| 397 |
+
def find_candidates(
|
| 398 |
+
self,
|
| 399 |
+
identifier: str,
|
| 400 |
+
requirements: Mapping[str, Iterable[Requirement]],
|
| 401 |
+
incompatibilities: Mapping[str, Iterator[Candidate]],
|
| 402 |
+
constraint: Constraint,
|
| 403 |
+
prefers_installed: bool,
|
| 404 |
+
is_satisfied_by: Callable[[Requirement, Candidate], bool],
|
| 405 |
+
) -> Iterable[Candidate]:
|
| 406 |
+
# Collect basic lookup information from the requirements.
|
| 407 |
+
explicit_candidates: Set[Candidate] = set()
|
| 408 |
+
ireqs: List[InstallRequirement] = []
|
| 409 |
+
for req in requirements[identifier]:
|
| 410 |
+
cand, ireq = req.get_candidate_lookup()
|
| 411 |
+
if cand is not None:
|
| 412 |
+
explicit_candidates.add(cand)
|
| 413 |
+
if ireq is not None:
|
| 414 |
+
ireqs.append(ireq)
|
| 415 |
+
|
| 416 |
+
# If the current identifier contains extras, add requires and explicit
|
| 417 |
+
# candidates from entries from extra-less identifier.
|
| 418 |
+
with contextlib.suppress(InvalidRequirement):
|
| 419 |
+
parsed_requirement = get_requirement(identifier)
|
| 420 |
+
if parsed_requirement.name != identifier:
|
| 421 |
+
explicit_candidates.update(
|
| 422 |
+
self._iter_explicit_candidates_from_base(
|
| 423 |
+
requirements.get(parsed_requirement.name, ()),
|
| 424 |
+
frozenset(parsed_requirement.extras),
|
| 425 |
+
),
|
| 426 |
+
)
|
| 427 |
+
for req in requirements.get(parsed_requirement.name, []):
|
| 428 |
+
_, ireq = req.get_candidate_lookup()
|
| 429 |
+
if ireq is not None:
|
| 430 |
+
ireqs.append(ireq)
|
| 431 |
+
|
| 432 |
+
# Add explicit candidates from constraints. We only do this if there are
|
| 433 |
+
# known ireqs, which represent requirements not already explicit. If
|
| 434 |
+
# there are no ireqs, we're constraining already-explicit requirements,
|
| 435 |
+
# which is handled later when we return the explicit candidates.
|
| 436 |
+
if ireqs:
|
| 437 |
+
try:
|
| 438 |
+
explicit_candidates.update(
|
| 439 |
+
self._iter_candidates_from_constraints(
|
| 440 |
+
identifier,
|
| 441 |
+
constraint,
|
| 442 |
+
template=ireqs[0],
|
| 443 |
+
),
|
| 444 |
+
)
|
| 445 |
+
except UnsupportedWheel:
|
| 446 |
+
# If we're constrained to install a wheel incompatible with the
|
| 447 |
+
# target architecture, no candidates will ever be valid.
|
| 448 |
+
return ()
|
| 449 |
+
|
| 450 |
+
# Since we cache all the candidates, incompatibility identification
|
| 451 |
+
# can be made quicker by comparing only the id() values.
|
| 452 |
+
incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())}
|
| 453 |
+
|
| 454 |
+
# If none of the requirements want an explicit candidate, we can ask
|
| 455 |
+
# the finder for candidates.
|
| 456 |
+
if not explicit_candidates:
|
| 457 |
+
return self._iter_found_candidates(
|
| 458 |
+
ireqs,
|
| 459 |
+
constraint.specifier,
|
| 460 |
+
constraint.hashes,
|
| 461 |
+
prefers_installed,
|
| 462 |
+
incompat_ids,
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
return (
|
| 466 |
+
c
|
| 467 |
+
for c in explicit_candidates
|
| 468 |
+
if id(c) not in incompat_ids
|
| 469 |
+
and constraint.is_satisfied_by(c)
|
| 470 |
+
and all(is_satisfied_by(req, c) for req in requirements[identifier])
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
def _make_requirements_from_install_req(
|
| 474 |
+
self, ireq: InstallRequirement, requested_extras: Iterable[str]
|
| 475 |
+
) -> Iterator[Requirement]:
|
| 476 |
+
"""
|
| 477 |
+
Returns requirement objects associated with the given InstallRequirement. In
|
| 478 |
+
most cases this will be a single object but the following special cases exist:
|
| 479 |
+
- the InstallRequirement has markers that do not apply -> result is empty
|
| 480 |
+
- the InstallRequirement has both a constraint (or link) and extras
|
| 481 |
+
-> result is split in two requirement objects: one with the constraint
|
| 482 |
+
(or link) and one with the extra. This allows centralized constraint
|
| 483 |
+
handling for the base, resulting in fewer candidate rejections.
|
| 484 |
+
"""
|
| 485 |
+
if not ireq.match_markers(requested_extras):
|
| 486 |
+
logger.info(
|
| 487 |
+
"Ignoring %s: markers '%s' don't match your environment",
|
| 488 |
+
ireq.name,
|
| 489 |
+
ireq.markers,
|
| 490 |
+
)
|
| 491 |
+
elif not ireq.link:
|
| 492 |
+
if ireq.extras and ireq.req is not None and ireq.req.specifier:
|
| 493 |
+
yield SpecifierWithoutExtrasRequirement(ireq)
|
| 494 |
+
yield SpecifierRequirement(ireq)
|
| 495 |
+
else:
|
| 496 |
+
self._fail_if_link_is_unsupported_wheel(ireq.link)
|
| 497 |
+
# Always make the link candidate for the base requirement to make it
|
| 498 |
+
# available to `find_candidates` for explicit candidate lookup for any
|
| 499 |
+
# set of extras.
|
| 500 |
+
# The extras are required separately via a second requirement.
|
| 501 |
+
cand = self._make_base_candidate_from_link(
|
| 502 |
+
ireq.link,
|
| 503 |
+
template=install_req_drop_extras(ireq) if ireq.extras else ireq,
|
| 504 |
+
name=canonicalize_name(ireq.name) if ireq.name else None,
|
| 505 |
+
version=None,
|
| 506 |
+
)
|
| 507 |
+
if cand is None:
|
| 508 |
+
# There's no way we can satisfy a URL requirement if the underlying
|
| 509 |
+
# candidate fails to build. An unnamed URL must be user-supplied, so
|
| 510 |
+
# we fail eagerly. If the URL is named, an unsatisfiable requirement
|
| 511 |
+
# can make the resolver do the right thing, either backtrack (and
|
| 512 |
+
# maybe find some other requirement that's buildable) or raise a
|
| 513 |
+
# ResolutionImpossible eventually.
|
| 514 |
+
if not ireq.name:
|
| 515 |
+
raise self._build_failures[ireq.link]
|
| 516 |
+
yield UnsatisfiableRequirement(canonicalize_name(ireq.name))
|
| 517 |
+
else:
|
| 518 |
+
# require the base from the link
|
| 519 |
+
yield self.make_requirement_from_candidate(cand)
|
| 520 |
+
if ireq.extras:
|
| 521 |
+
# require the extras on top of the base candidate
|
| 522 |
+
yield self.make_requirement_from_candidate(
|
| 523 |
+
self._make_extras_candidate(cand, frozenset(ireq.extras))
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
def collect_root_requirements(
|
| 527 |
+
self, root_ireqs: List[InstallRequirement]
|
| 528 |
+
) -> CollectedRootRequirements:
|
| 529 |
+
collected = CollectedRootRequirements([], {}, {})
|
| 530 |
+
for i, ireq in enumerate(root_ireqs):
|
| 531 |
+
if ireq.constraint:
|
| 532 |
+
# Ensure we only accept valid constraints
|
| 533 |
+
problem = check_invalid_constraint_type(ireq)
|
| 534 |
+
if problem:
|
| 535 |
+
raise InstallationError(problem)
|
| 536 |
+
if not ireq.match_markers():
|
| 537 |
+
continue
|
| 538 |
+
assert ireq.name, "Constraint must be named"
|
| 539 |
+
name = canonicalize_name(ireq.name)
|
| 540 |
+
if name in collected.constraints:
|
| 541 |
+
collected.constraints[name] &= ireq
|
| 542 |
+
else:
|
| 543 |
+
collected.constraints[name] = Constraint.from_ireq(ireq)
|
| 544 |
+
else:
|
| 545 |
+
reqs = list(
|
| 546 |
+
self._make_requirements_from_install_req(
|
| 547 |
+
ireq,
|
| 548 |
+
requested_extras=(),
|
| 549 |
+
)
|
| 550 |
+
)
|
| 551 |
+
if not reqs:
|
| 552 |
+
continue
|
| 553 |
+
template = reqs[0]
|
| 554 |
+
if ireq.user_supplied and template.name not in collected.user_requested:
|
| 555 |
+
collected.user_requested[template.name] = i
|
| 556 |
+
collected.requirements.extend(reqs)
|
| 557 |
+
# Put requirements with extras at the end of the root requires. This does not
|
| 558 |
+
# affect resolvelib's picking preference but it does affect its initial criteria
|
| 559 |
+
# population: by putting extras at the end we enable the candidate finder to
|
| 560 |
+
# present resolvelib with a smaller set of candidates to resolvelib, already
|
| 561 |
+
# taking into account any non-transient constraints on the associated base. This
|
| 562 |
+
# means resolvelib will have fewer candidates to visit and reject.
|
| 563 |
+
# Python's list sort is stable, meaning relative order is kept for objects with
|
| 564 |
+
# the same key.
|
| 565 |
+
collected.requirements.sort(key=lambda r: r.name != r.project_name)
|
| 566 |
+
return collected
|
| 567 |
+
|
| 568 |
+
def make_requirement_from_candidate(
|
| 569 |
+
self, candidate: Candidate
|
| 570 |
+
) -> ExplicitRequirement:
|
| 571 |
+
return ExplicitRequirement(candidate)
|
| 572 |
+
|
| 573 |
+
def make_requirements_from_spec(
|
| 574 |
+
self,
|
| 575 |
+
specifier: str,
|
| 576 |
+
comes_from: Optional[InstallRequirement],
|
| 577 |
+
requested_extras: Iterable[str] = (),
|
| 578 |
+
) -> Iterator[Requirement]:
|
| 579 |
+
"""
|
| 580 |
+
Returns requirement objects associated with the given specifier. In most cases
|
| 581 |
+
this will be a single object but the following special cases exist:
|
| 582 |
+
- the specifier has markers that do not apply -> result is empty
|
| 583 |
+
- the specifier has both a constraint and extras -> result is split
|
| 584 |
+
in two requirement objects: one with the constraint and one with the
|
| 585 |
+
extra. This allows centralized constraint handling for the base,
|
| 586 |
+
resulting in fewer candidate rejections.
|
| 587 |
+
"""
|
| 588 |
+
ireq = self._make_install_req_from_spec(specifier, comes_from)
|
| 589 |
+
return self._make_requirements_from_install_req(ireq, requested_extras)
|
| 590 |
+
|
| 591 |
+
def make_requires_python_requirement(
|
| 592 |
+
self,
|
| 593 |
+
specifier: SpecifierSet,
|
| 594 |
+
) -> Optional[Requirement]:
|
| 595 |
+
if self._ignore_requires_python:
|
| 596 |
+
return None
|
| 597 |
+
# Don't bother creating a dependency for an empty Requires-Python.
|
| 598 |
+
if not str(specifier):
|
| 599 |
+
return None
|
| 600 |
+
return RequiresPythonRequirement(specifier, self._python_candidate)
|
| 601 |
+
|
| 602 |
+
def get_wheel_cache_entry(
|
| 603 |
+
self, link: Link, name: Optional[str]
|
| 604 |
+
) -> Optional[CacheEntry]:
|
| 605 |
+
"""Look up the link in the wheel cache.
|
| 606 |
+
|
| 607 |
+
If ``preparer.require_hashes`` is True, don't use the wheel cache,
|
| 608 |
+
because cached wheels, always built locally, have different hashes
|
| 609 |
+
than the files downloaded from the index server and thus throw false
|
| 610 |
+
hash mismatches. Furthermore, cached wheels at present have
|
| 611 |
+
nondeterministic contents due to file modification times.
|
| 612 |
+
"""
|
| 613 |
+
if self._wheel_cache is None:
|
| 614 |
+
return None
|
| 615 |
+
return self._wheel_cache.get_cache_entry(
|
| 616 |
+
link=link,
|
| 617 |
+
package_name=name,
|
| 618 |
+
supported_tags=self._supported_tags_cache,
|
| 619 |
+
)
|
| 620 |
+
|
| 621 |
+
def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]:
|
| 622 |
+
# TODO: Are there more cases this needs to return True? Editable?
|
| 623 |
+
dist = self._installed_dists.get(candidate.project_name)
|
| 624 |
+
if dist is None: # Not installed, no uninstallation required.
|
| 625 |
+
return None
|
| 626 |
+
|
| 627 |
+
# We're installing into global site. The current installation must
|
| 628 |
+
# be uninstalled, no matter it's in global or user site, because the
|
| 629 |
+
# user site installation has precedence over global.
|
| 630 |
+
if not self._use_user_site:
|
| 631 |
+
return dist
|
| 632 |
+
|
| 633 |
+
# We're installing into user site. Remove the user site installation.
|
| 634 |
+
if dist.in_usersite:
|
| 635 |
+
return dist
|
| 636 |
+
|
| 637 |
+
# We're installing into user site, but the installed incompatible
|
| 638 |
+
# package is in global site. We can't uninstall that, and would let
|
| 639 |
+
# the new user installation to "shadow" it. But shadowing won't work
|
| 640 |
+
# in virtual environments, so we error out.
|
| 641 |
+
if running_under_virtualenv() and dist.in_site_packages:
|
| 642 |
+
message = (
|
| 643 |
+
f"Will not install to the user site because it will lack "
|
| 644 |
+
f"sys.path precedence to {dist.raw_name} in {dist.location}"
|
| 645 |
+
)
|
| 646 |
+
raise InstallationError(message)
|
| 647 |
+
return None
|
| 648 |
+
|
| 649 |
+
def _report_requires_python_error(
|
| 650 |
+
self, causes: Sequence["ConflictCause"]
|
| 651 |
+
) -> UnsupportedPythonVersion:
|
| 652 |
+
assert causes, "Requires-Python error reported with no cause"
|
| 653 |
+
|
| 654 |
+
version = self._python_candidate.version
|
| 655 |
+
|
| 656 |
+
if len(causes) == 1:
|
| 657 |
+
specifier = str(causes[0].requirement.specifier)
|
| 658 |
+
message = (
|
| 659 |
+
f"Package {causes[0].parent.name!r} requires a different "
|
| 660 |
+
f"Python: {version} not in {specifier!r}"
|
| 661 |
+
)
|
| 662 |
+
return UnsupportedPythonVersion(message)
|
| 663 |
+
|
| 664 |
+
message = f"Packages require a different Python. {version} not in:"
|
| 665 |
+
for cause in causes:
|
| 666 |
+
package = cause.parent.format_for_error()
|
| 667 |
+
specifier = str(cause.requirement.specifier)
|
| 668 |
+
message += f"\n{specifier!r} (required by {package})"
|
| 669 |
+
return UnsupportedPythonVersion(message)
|
| 670 |
+
|
| 671 |
+
def _report_single_requirement_conflict(
|
| 672 |
+
self, req: Requirement, parent: Optional[Candidate]
|
| 673 |
+
) -> DistributionNotFound:
|
| 674 |
+
if parent is None:
|
| 675 |
+
req_disp = str(req)
|
| 676 |
+
else:
|
| 677 |
+
req_disp = f"{req} (from {parent.name})"
|
| 678 |
+
|
| 679 |
+
cands = self._finder.find_all_candidates(req.project_name)
|
| 680 |
+
skipped_by_requires_python = self._finder.requires_python_skipped_reasons()
|
| 681 |
+
|
| 682 |
+
versions_set: Set[Version] = set()
|
| 683 |
+
yanked_versions_set: Set[Version] = set()
|
| 684 |
+
for c in cands:
|
| 685 |
+
is_yanked = c.link.is_yanked if c.link else False
|
| 686 |
+
if is_yanked:
|
| 687 |
+
yanked_versions_set.add(c.version)
|
| 688 |
+
else:
|
| 689 |
+
versions_set.add(c.version)
|
| 690 |
+
|
| 691 |
+
versions = [str(v) for v in sorted(versions_set)]
|
| 692 |
+
yanked_versions = [str(v) for v in sorted(yanked_versions_set)]
|
| 693 |
+
|
| 694 |
+
if yanked_versions:
|
| 695 |
+
# Saying "version X is yanked" isn't entirely accurate.
|
| 696 |
+
# https://github.com/pypa/pip/issues/11745#issuecomment-1402805842
|
| 697 |
+
logger.critical(
|
| 698 |
+
"Ignored the following yanked versions: %s",
|
| 699 |
+
", ".join(yanked_versions) or "none",
|
| 700 |
+
)
|
| 701 |
+
if skipped_by_requires_python:
|
| 702 |
+
logger.critical(
|
| 703 |
+
"Ignored the following versions that require a different python "
|
| 704 |
+
"version: %s",
|
| 705 |
+
"; ".join(skipped_by_requires_python) or "none",
|
| 706 |
+
)
|
| 707 |
+
logger.critical(
|
| 708 |
+
"Could not find a version that satisfies the requirement %s "
|
| 709 |
+
"(from versions: %s)",
|
| 710 |
+
req_disp,
|
| 711 |
+
", ".join(versions) or "none",
|
| 712 |
+
)
|
| 713 |
+
if str(req) == "requirements.txt":
|
| 714 |
+
logger.info(
|
| 715 |
+
"HINT: You are attempting to install a package literally "
|
| 716 |
+
'named "requirements.txt" (which cannot exist). Consider '
|
| 717 |
+
"using the '-r' flag to install the packages listed in "
|
| 718 |
+
"requirements.txt"
|
| 719 |
+
)
|
| 720 |
+
|
| 721 |
+
return DistributionNotFound(f"No matching distribution found for {req}")
|
| 722 |
+
|
| 723 |
+
def get_installation_error(
|
| 724 |
+
self,
|
| 725 |
+
e: "ResolutionImpossible[Requirement, Candidate]",
|
| 726 |
+
constraints: Dict[str, Constraint],
|
| 727 |
+
) -> InstallationError:
|
| 728 |
+
assert e.causes, "Installation error reported with no cause"
|
| 729 |
+
|
| 730 |
+
# If one of the things we can't solve is "we need Python X.Y",
|
| 731 |
+
# that is what we report.
|
| 732 |
+
requires_python_causes = [
|
| 733 |
+
cause
|
| 734 |
+
for cause in e.causes
|
| 735 |
+
if isinstance(cause.requirement, RequiresPythonRequirement)
|
| 736 |
+
and not cause.requirement.is_satisfied_by(self._python_candidate)
|
| 737 |
+
]
|
| 738 |
+
if requires_python_causes:
|
| 739 |
+
# The comprehension above makes sure all Requirement instances are
|
| 740 |
+
# RequiresPythonRequirement, so let's cast for convenience.
|
| 741 |
+
return self._report_requires_python_error(
|
| 742 |
+
cast("Sequence[ConflictCause]", requires_python_causes),
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
# Otherwise, we have a set of causes which can't all be satisfied
|
| 746 |
+
# at once.
|
| 747 |
+
|
| 748 |
+
# The simplest case is when we have *one* cause that can't be
|
| 749 |
+
# satisfied. We just report that case.
|
| 750 |
+
if len(e.causes) == 1:
|
| 751 |
+
req, parent = next(iter(e.causes))
|
| 752 |
+
if req.name not in constraints:
|
| 753 |
+
return self._report_single_requirement_conflict(req, parent)
|
| 754 |
+
|
| 755 |
+
# OK, we now have a list of requirements that can't all be
|
| 756 |
+
# satisfied at once.
|
| 757 |
+
|
| 758 |
+
# A couple of formatting helpers
|
| 759 |
+
def text_join(parts: List[str]) -> str:
|
| 760 |
+
if len(parts) == 1:
|
| 761 |
+
return parts[0]
|
| 762 |
+
|
| 763 |
+
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
| 764 |
+
|
| 765 |
+
def describe_trigger(parent: Candidate) -> str:
|
| 766 |
+
ireq = parent.get_install_requirement()
|
| 767 |
+
if not ireq or not ireq.comes_from:
|
| 768 |
+
return f"{parent.name}=={parent.version}"
|
| 769 |
+
if isinstance(ireq.comes_from, InstallRequirement):
|
| 770 |
+
return str(ireq.comes_from.name)
|
| 771 |
+
return str(ireq.comes_from)
|
| 772 |
+
|
| 773 |
+
triggers = set()
|
| 774 |
+
for req, parent in e.causes:
|
| 775 |
+
if parent is None:
|
| 776 |
+
# This is a root requirement, so we can report it directly
|
| 777 |
+
trigger = req.format_for_error()
|
| 778 |
+
else:
|
| 779 |
+
trigger = describe_trigger(parent)
|
| 780 |
+
triggers.add(trigger)
|
| 781 |
+
|
| 782 |
+
if triggers:
|
| 783 |
+
info = text_join(sorted(triggers))
|
| 784 |
+
else:
|
| 785 |
+
info = "the requested packages"
|
| 786 |
+
|
| 787 |
+
msg = (
|
| 788 |
+
f"Cannot install {info} because these package versions "
|
| 789 |
+
"have conflicting dependencies."
|
| 790 |
+
)
|
| 791 |
+
logger.critical(msg)
|
| 792 |
+
msg = "\nThe conflict is caused by:"
|
| 793 |
+
|
| 794 |
+
relevant_constraints = set()
|
| 795 |
+
for req, parent in e.causes:
|
| 796 |
+
if req.name in constraints:
|
| 797 |
+
relevant_constraints.add(req.name)
|
| 798 |
+
msg = msg + "\n "
|
| 799 |
+
if parent:
|
| 800 |
+
msg = msg + f"{parent.name} {parent.version} depends on "
|
| 801 |
+
else:
|
| 802 |
+
msg = msg + "The user requested "
|
| 803 |
+
msg = msg + req.format_for_error()
|
| 804 |
+
for key in relevant_constraints:
|
| 805 |
+
spec = constraints[key].specifier
|
| 806 |
+
msg += f"\n The user requested (constraint) {key}{spec}"
|
| 807 |
+
|
| 808 |
+
msg = (
|
| 809 |
+
msg
|
| 810 |
+
+ "\n\n"
|
| 811 |
+
+ "To fix this you could try to:\n"
|
| 812 |
+
+ "1. loosen the range of package versions you've specified\n"
|
| 813 |
+
+ "2. remove package versions to allow pip to attempt to solve "
|
| 814 |
+
+ "the dependency conflict\n"
|
| 815 |
+
)
|
| 816 |
+
|
| 817 |
+
logger.info(msg)
|
| 818 |
+
|
| 819 |
+
return DistributionNotFound(
|
| 820 |
+
"ResolutionImpossible: for help visit "
|
| 821 |
+
"https://pip.pypa.io/en/latest/topics/dependency-resolution/"
|
| 822 |
+
"#dealing-with-dependency-conflicts"
|
| 823 |
+
)
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utilities to lazily create and visit candidates found.
|
| 2 |
+
|
| 3 |
+
Creating and visiting a candidate is a *very* costly operation. It involves
|
| 4 |
+
fetching, extracting, potentially building modules from source, and verifying
|
| 5 |
+
distribution metadata. It is therefore crucial for performance to keep
|
| 6 |
+
everything here lazy all the way down, so we only touch candidates that we
|
| 7 |
+
absolutely need, and not "download the world" when we only need one version of
|
| 8 |
+
something.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import logging
|
| 12 |
+
from collections.abc import Sequence
|
| 13 |
+
from typing import Any, Callable, Iterator, Optional, Set, Tuple
|
| 14 |
+
|
| 15 |
+
from pip._vendor.packaging.version import _BaseVersion
|
| 16 |
+
|
| 17 |
+
from pip._internal.exceptions import MetadataInvalid
|
| 18 |
+
|
| 19 |
+
from .base import Candidate
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _iter_built(infos: Iterator[IndexCandidateInfo]) -> Iterator[Candidate]:
|
| 27 |
+
"""Iterator for ``FoundCandidates``.
|
| 28 |
+
|
| 29 |
+
This iterator is used when the package is not already installed. Candidates
|
| 30 |
+
from index come later in their normal ordering.
|
| 31 |
+
"""
|
| 32 |
+
versions_found: Set[_BaseVersion] = set()
|
| 33 |
+
for version, func in infos:
|
| 34 |
+
if version in versions_found:
|
| 35 |
+
continue
|
| 36 |
+
try:
|
| 37 |
+
candidate = func()
|
| 38 |
+
except MetadataInvalid as e:
|
| 39 |
+
logger.warning(
|
| 40 |
+
"Ignoring version %s of %s since it has invalid metadata:\n"
|
| 41 |
+
"%s\n"
|
| 42 |
+
"Please use pip<24.1 if you need to use this version.",
|
| 43 |
+
version,
|
| 44 |
+
e.ireq.name,
|
| 45 |
+
e,
|
| 46 |
+
)
|
| 47 |
+
# Mark version as found to avoid trying other candidates with the same
|
| 48 |
+
# version, since they most likely have invalid metadata as well.
|
| 49 |
+
versions_found.add(version)
|
| 50 |
+
else:
|
| 51 |
+
if candidate is None:
|
| 52 |
+
continue
|
| 53 |
+
yield candidate
|
| 54 |
+
versions_found.add(version)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _iter_built_with_prepended(
|
| 58 |
+
installed: Candidate, infos: Iterator[IndexCandidateInfo]
|
| 59 |
+
) -> Iterator[Candidate]:
|
| 60 |
+
"""Iterator for ``FoundCandidates``.
|
| 61 |
+
|
| 62 |
+
This iterator is used when the resolver prefers the already-installed
|
| 63 |
+
candidate and NOT to upgrade. The installed candidate is therefore
|
| 64 |
+
always yielded first, and candidates from index come later in their
|
| 65 |
+
normal ordering, except skipped when the version is already installed.
|
| 66 |
+
"""
|
| 67 |
+
yield installed
|
| 68 |
+
versions_found: Set[_BaseVersion] = {installed.version}
|
| 69 |
+
for version, func in infos:
|
| 70 |
+
if version in versions_found:
|
| 71 |
+
continue
|
| 72 |
+
candidate = func()
|
| 73 |
+
if candidate is None:
|
| 74 |
+
continue
|
| 75 |
+
yield candidate
|
| 76 |
+
versions_found.add(version)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _iter_built_with_inserted(
|
| 80 |
+
installed: Candidate, infos: Iterator[IndexCandidateInfo]
|
| 81 |
+
) -> Iterator[Candidate]:
|
| 82 |
+
"""Iterator for ``FoundCandidates``.
|
| 83 |
+
|
| 84 |
+
This iterator is used when the resolver prefers to upgrade an
|
| 85 |
+
already-installed package. Candidates from index are returned in their
|
| 86 |
+
normal ordering, except replaced when the version is already installed.
|
| 87 |
+
|
| 88 |
+
The implementation iterates through and yields other candidates, inserting
|
| 89 |
+
the installed candidate exactly once before we start yielding older or
|
| 90 |
+
equivalent candidates, or after all other candidates if they are all newer.
|
| 91 |
+
"""
|
| 92 |
+
versions_found: Set[_BaseVersion] = set()
|
| 93 |
+
for version, func in infos:
|
| 94 |
+
if version in versions_found:
|
| 95 |
+
continue
|
| 96 |
+
# If the installed candidate is better, yield it first.
|
| 97 |
+
if installed.version >= version:
|
| 98 |
+
yield installed
|
| 99 |
+
versions_found.add(installed.version)
|
| 100 |
+
candidate = func()
|
| 101 |
+
if candidate is None:
|
| 102 |
+
continue
|
| 103 |
+
yield candidate
|
| 104 |
+
versions_found.add(version)
|
| 105 |
+
|
| 106 |
+
# If the installed candidate is older than all other candidates.
|
| 107 |
+
if installed.version not in versions_found:
|
| 108 |
+
yield installed
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class FoundCandidates(Sequence[Candidate]):
|
| 112 |
+
"""A lazy sequence to provide candidates to the resolver.
|
| 113 |
+
|
| 114 |
+
The intended usage is to return this from `find_matches()` so the resolver
|
| 115 |
+
can iterate through the sequence multiple times, but only access the index
|
| 116 |
+
page when remote packages are actually needed. This improve performances
|
| 117 |
+
when suitable candidates are already installed on disk.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
def __init__(
|
| 121 |
+
self,
|
| 122 |
+
get_infos: Callable[[], Iterator[IndexCandidateInfo]],
|
| 123 |
+
installed: Optional[Candidate],
|
| 124 |
+
prefers_installed: bool,
|
| 125 |
+
incompatible_ids: Set[int],
|
| 126 |
+
):
|
| 127 |
+
self._get_infos = get_infos
|
| 128 |
+
self._installed = installed
|
| 129 |
+
self._prefers_installed = prefers_installed
|
| 130 |
+
self._incompatible_ids = incompatible_ids
|
| 131 |
+
self._bool: Optional[bool] = None
|
| 132 |
+
|
| 133 |
+
def __getitem__(self, index: Any) -> Any:
|
| 134 |
+
# Implemented to satisfy the ABC check. This is not needed by the
|
| 135 |
+
# resolver, and should not be used by the provider either (for
|
| 136 |
+
# performance reasons).
|
| 137 |
+
raise NotImplementedError("don't do this")
|
| 138 |
+
|
| 139 |
+
def __iter__(self) -> Iterator[Candidate]:
|
| 140 |
+
infos = self._get_infos()
|
| 141 |
+
if not self._installed:
|
| 142 |
+
iterator = _iter_built(infos)
|
| 143 |
+
elif self._prefers_installed:
|
| 144 |
+
iterator = _iter_built_with_prepended(self._installed, infos)
|
| 145 |
+
else:
|
| 146 |
+
iterator = _iter_built_with_inserted(self._installed, infos)
|
| 147 |
+
return (c for c in iterator if id(c) not in self._incompatible_ids)
|
| 148 |
+
|
| 149 |
+
def __len__(self) -> int:
|
| 150 |
+
# Implemented to satisfy the ABC check. This is not needed by the
|
| 151 |
+
# resolver, and should not be used by the provider either (for
|
| 152 |
+
# performance reasons).
|
| 153 |
+
raise NotImplementedError("don't do this")
|
| 154 |
+
|
| 155 |
+
def __bool__(self) -> bool:
|
| 156 |
+
if self._bool is not None:
|
| 157 |
+
return self._bool
|
| 158 |
+
|
| 159 |
+
if self._prefers_installed and self._installed:
|
| 160 |
+
self._bool = True
|
| 161 |
+
return True
|
| 162 |
+
|
| 163 |
+
self._bool = any(self)
|
| 164 |
+
return self._bool
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/provider.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
from typing import (
|
| 4 |
+
TYPE_CHECKING,
|
| 5 |
+
Dict,
|
| 6 |
+
Iterable,
|
| 7 |
+
Iterator,
|
| 8 |
+
Mapping,
|
| 9 |
+
Optional,
|
| 10 |
+
Sequence,
|
| 11 |
+
Tuple,
|
| 12 |
+
TypeVar,
|
| 13 |
+
Union,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
from pip._vendor.resolvelib.providers import AbstractProvider
|
| 17 |
+
|
| 18 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 19 |
+
|
| 20 |
+
from .base import Candidate, Constraint, Requirement
|
| 21 |
+
from .candidates import REQUIRES_PYTHON_IDENTIFIER
|
| 22 |
+
from .factory import Factory
|
| 23 |
+
from .requirements import ExplicitRequirement
|
| 24 |
+
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from pip._vendor.resolvelib.providers import Preference
|
| 27 |
+
from pip._vendor.resolvelib.resolvers import RequirementInformation
|
| 28 |
+
|
| 29 |
+
PreferenceInformation = RequirementInformation[Requirement, Candidate]
|
| 30 |
+
|
| 31 |
+
_ProviderBase = AbstractProvider[Requirement, Candidate, str]
|
| 32 |
+
else:
|
| 33 |
+
_ProviderBase = AbstractProvider
|
| 34 |
+
|
| 35 |
+
# Notes on the relationship between the provider, the factory, and the
|
| 36 |
+
# candidate and requirement classes.
|
| 37 |
+
#
|
| 38 |
+
# The provider is a direct implementation of the resolvelib class. Its role
|
| 39 |
+
# is to deliver the API that resolvelib expects.
|
| 40 |
+
#
|
| 41 |
+
# Rather than work with completely abstract "requirement" and "candidate"
|
| 42 |
+
# concepts as resolvelib does, pip has concrete classes implementing these two
|
| 43 |
+
# ideas. The API of Requirement and Candidate objects are defined in the base
|
| 44 |
+
# classes, but essentially map fairly directly to the equivalent provider
|
| 45 |
+
# methods. In particular, `find_matches` and `is_satisfied_by` are
|
| 46 |
+
# requirement methods, and `get_dependencies` is a candidate method.
|
| 47 |
+
#
|
| 48 |
+
# The factory is the interface to pip's internal mechanisms. It is stateless,
|
| 49 |
+
# and is created by the resolver and held as a property of the provider. It is
|
| 50 |
+
# responsible for creating Requirement and Candidate objects, and provides
|
| 51 |
+
# services to those objects (access to pip's finder and preparer).
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
D = TypeVar("D")
|
| 55 |
+
V = TypeVar("V")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _get_with_identifier(
|
| 59 |
+
mapping: Mapping[str, V],
|
| 60 |
+
identifier: str,
|
| 61 |
+
default: D,
|
| 62 |
+
) -> Union[D, V]:
|
| 63 |
+
"""Get item from a package name lookup mapping with a resolver identifier.
|
| 64 |
+
|
| 65 |
+
This extra logic is needed when the target mapping is keyed by package
|
| 66 |
+
name, which cannot be directly looked up with an identifier (which may
|
| 67 |
+
contain requested extras). Additional logic is added to also look up a value
|
| 68 |
+
by "cleaning up" the extras from the identifier.
|
| 69 |
+
"""
|
| 70 |
+
if identifier in mapping:
|
| 71 |
+
return mapping[identifier]
|
| 72 |
+
# HACK: Theoretically we should check whether this identifier is a valid
|
| 73 |
+
# "NAME[EXTRAS]" format, and parse out the name part with packaging or
|
| 74 |
+
# some regular expression. But since pip's resolver only spits out three
|
| 75 |
+
# kinds of identifiers: normalized PEP 503 names, normalized names plus
|
| 76 |
+
# extras, and Requires-Python, we can cheat a bit here.
|
| 77 |
+
name, open_bracket, _ = identifier.partition("[")
|
| 78 |
+
if open_bracket and name in mapping:
|
| 79 |
+
return mapping[name]
|
| 80 |
+
return default
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class PipProvider(_ProviderBase):
|
| 84 |
+
"""Pip's provider implementation for resolvelib.
|
| 85 |
+
|
| 86 |
+
:params constraints: A mapping of constraints specified by the user. Keys
|
| 87 |
+
are canonicalized project names.
|
| 88 |
+
:params ignore_dependencies: Whether the user specified ``--no-deps``.
|
| 89 |
+
:params upgrade_strategy: The user-specified upgrade strategy.
|
| 90 |
+
:params user_requested: A set of canonicalized package names that the user
|
| 91 |
+
supplied for pip to install/upgrade.
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def __init__(
|
| 95 |
+
self,
|
| 96 |
+
factory: Factory,
|
| 97 |
+
constraints: Dict[str, Constraint],
|
| 98 |
+
ignore_dependencies: bool,
|
| 99 |
+
upgrade_strategy: str,
|
| 100 |
+
user_requested: Dict[str, int],
|
| 101 |
+
) -> None:
|
| 102 |
+
self._factory = factory
|
| 103 |
+
self._constraints = constraints
|
| 104 |
+
self._ignore_dependencies = ignore_dependencies
|
| 105 |
+
self._upgrade_strategy = upgrade_strategy
|
| 106 |
+
self._user_requested = user_requested
|
| 107 |
+
|
| 108 |
+
def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str:
|
| 109 |
+
return requirement_or_candidate.name
|
| 110 |
+
|
| 111 |
+
def narrow_requirement_selection(
|
| 112 |
+
self,
|
| 113 |
+
identifiers: Iterable[str],
|
| 114 |
+
resolutions: Mapping[str, Candidate],
|
| 115 |
+
candidates: Mapping[str, Iterator[Candidate]],
|
| 116 |
+
information: Mapping[str, Iterator["PreferenceInformation"]],
|
| 117 |
+
backtrack_causes: Sequence["PreferenceInformation"],
|
| 118 |
+
) -> Iterable[str]:
|
| 119 |
+
"""Produce a subset of identifiers that should be considered before others.
|
| 120 |
+
|
| 121 |
+
Currently pip narrows the following selection:
|
| 122 |
+
* Requires-Python, if present is always returned by itself
|
| 123 |
+
* Backtrack causes are considered next because they can be identified
|
| 124 |
+
in linear time here, whereas because get_preference() is called
|
| 125 |
+
for each identifier, it would be quadratic to check for them there.
|
| 126 |
+
Further, the current backtrack causes likely need to be resolved
|
| 127 |
+
before other requirements as a resolution can't be found while
|
| 128 |
+
there is a conflict.
|
| 129 |
+
"""
|
| 130 |
+
backtrack_identifiers = set()
|
| 131 |
+
for info in backtrack_causes:
|
| 132 |
+
backtrack_identifiers.add(info.requirement.name)
|
| 133 |
+
if info.parent is not None:
|
| 134 |
+
backtrack_identifiers.add(info.parent.name)
|
| 135 |
+
|
| 136 |
+
current_backtrack_causes = []
|
| 137 |
+
for identifier in identifiers:
|
| 138 |
+
# Requires-Python has only one candidate and the check is basically
|
| 139 |
+
# free, so we always do it first to avoid needless work if it fails.
|
| 140 |
+
# This skips calling get_preference() for all other identifiers.
|
| 141 |
+
if identifier == REQUIRES_PYTHON_IDENTIFIER:
|
| 142 |
+
return [identifier]
|
| 143 |
+
|
| 144 |
+
# Check if this identifier is a backtrack cause
|
| 145 |
+
if identifier in backtrack_identifiers:
|
| 146 |
+
current_backtrack_causes.append(identifier)
|
| 147 |
+
continue
|
| 148 |
+
|
| 149 |
+
if current_backtrack_causes:
|
| 150 |
+
return current_backtrack_causes
|
| 151 |
+
|
| 152 |
+
return identifiers
|
| 153 |
+
|
| 154 |
+
def get_preference(
|
| 155 |
+
self,
|
| 156 |
+
identifier: str,
|
| 157 |
+
resolutions: Mapping[str, Candidate],
|
| 158 |
+
candidates: Mapping[str, Iterator[Candidate]],
|
| 159 |
+
information: Mapping[str, Iterable["PreferenceInformation"]],
|
| 160 |
+
backtrack_causes: Sequence["PreferenceInformation"],
|
| 161 |
+
) -> "Preference":
|
| 162 |
+
"""Produce a sort key for given requirement based on preference.
|
| 163 |
+
|
| 164 |
+
The lower the return value is, the more preferred this group of
|
| 165 |
+
arguments is.
|
| 166 |
+
|
| 167 |
+
Currently pip considers the following in order:
|
| 168 |
+
|
| 169 |
+
* Any requirement that is "direct", e.g., points to an explicit URL.
|
| 170 |
+
* Any requirement that is "pinned", i.e., contains the operator ``===``
|
| 171 |
+
or ``==`` without a wildcard.
|
| 172 |
+
* Any requirement that imposes an upper version limit, i.e., contains the
|
| 173 |
+
operator ``<``, ``<=``, ``~=``, or ``==`` with a wildcard. Because
|
| 174 |
+
pip prioritizes the latest version, preferring explicit upper bounds
|
| 175 |
+
can rule out infeasible candidates sooner. This does not imply that
|
| 176 |
+
upper bounds are good practice; they can make dependency management
|
| 177 |
+
and resolution harder.
|
| 178 |
+
* Order user-specified requirements as they are specified, placing
|
| 179 |
+
other requirements afterward.
|
| 180 |
+
* Any "non-free" requirement, i.e., one that contains at least one
|
| 181 |
+
operator, such as ``>=`` or ``!=``.
|
| 182 |
+
* Alphabetical order for consistency (aids debuggability).
|
| 183 |
+
"""
|
| 184 |
+
try:
|
| 185 |
+
next(iter(information[identifier]))
|
| 186 |
+
except StopIteration:
|
| 187 |
+
# There is no information for this identifier, so there's no known
|
| 188 |
+
# candidates.
|
| 189 |
+
has_information = False
|
| 190 |
+
else:
|
| 191 |
+
has_information = True
|
| 192 |
+
|
| 193 |
+
if not has_information:
|
| 194 |
+
direct = False
|
| 195 |
+
ireqs: Tuple[Optional[InstallRequirement], ...] = ()
|
| 196 |
+
else:
|
| 197 |
+
# Go through the information and for each requirement,
|
| 198 |
+
# check if it's explicit (e.g., a direct link) and get the
|
| 199 |
+
# InstallRequirement (the second element) from get_candidate_lookup()
|
| 200 |
+
directs, ireqs = zip(
|
| 201 |
+
*(
|
| 202 |
+
(isinstance(r, ExplicitRequirement), r.get_candidate_lookup()[1])
|
| 203 |
+
for r, _ in information[identifier]
|
| 204 |
+
)
|
| 205 |
+
)
|
| 206 |
+
direct = any(directs)
|
| 207 |
+
|
| 208 |
+
operators: list[tuple[str, str]] = [
|
| 209 |
+
(specifier.operator, specifier.version)
|
| 210 |
+
for specifier_set in (ireq.specifier for ireq in ireqs if ireq)
|
| 211 |
+
for specifier in specifier_set
|
| 212 |
+
]
|
| 213 |
+
|
| 214 |
+
pinned = any(((op[:2] == "==") and ("*" not in ver)) for op, ver in operators)
|
| 215 |
+
upper_bounded = any(
|
| 216 |
+
((op in ("<", "<=", "~=")) or (op == "==" and "*" in ver))
|
| 217 |
+
for op, ver in operators
|
| 218 |
+
)
|
| 219 |
+
unfree = bool(operators)
|
| 220 |
+
requested_order = self._user_requested.get(identifier, math.inf)
|
| 221 |
+
|
| 222 |
+
return (
|
| 223 |
+
not direct,
|
| 224 |
+
not pinned,
|
| 225 |
+
not upper_bounded,
|
| 226 |
+
requested_order,
|
| 227 |
+
not unfree,
|
| 228 |
+
identifier,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def find_matches(
|
| 232 |
+
self,
|
| 233 |
+
identifier: str,
|
| 234 |
+
requirements: Mapping[str, Iterator[Requirement]],
|
| 235 |
+
incompatibilities: Mapping[str, Iterator[Candidate]],
|
| 236 |
+
) -> Iterable[Candidate]:
|
| 237 |
+
def _eligible_for_upgrade(identifier: str) -> bool:
|
| 238 |
+
"""Are upgrades allowed for this project?
|
| 239 |
+
|
| 240 |
+
This checks the upgrade strategy, and whether the project was one
|
| 241 |
+
that the user specified in the command line, in order to decide
|
| 242 |
+
whether we should upgrade if there's a newer version available.
|
| 243 |
+
|
| 244 |
+
(Note that we don't need access to the `--upgrade` flag, because
|
| 245 |
+
an upgrade strategy of "to-satisfy-only" means that `--upgrade`
|
| 246 |
+
was not specified).
|
| 247 |
+
"""
|
| 248 |
+
if self._upgrade_strategy == "eager":
|
| 249 |
+
return True
|
| 250 |
+
elif self._upgrade_strategy == "only-if-needed":
|
| 251 |
+
user_order = _get_with_identifier(
|
| 252 |
+
self._user_requested,
|
| 253 |
+
identifier,
|
| 254 |
+
default=None,
|
| 255 |
+
)
|
| 256 |
+
return user_order is not None
|
| 257 |
+
return False
|
| 258 |
+
|
| 259 |
+
constraint = _get_with_identifier(
|
| 260 |
+
self._constraints,
|
| 261 |
+
identifier,
|
| 262 |
+
default=Constraint.empty(),
|
| 263 |
+
)
|
| 264 |
+
return self._factory.find_candidates(
|
| 265 |
+
identifier=identifier,
|
| 266 |
+
requirements=requirements,
|
| 267 |
+
constraint=constraint,
|
| 268 |
+
prefers_installed=(not _eligible_for_upgrade(identifier)),
|
| 269 |
+
incompatibilities=incompatibilities,
|
| 270 |
+
is_satisfied_by=self.is_satisfied_by,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
@staticmethod
|
| 274 |
+
@lru_cache(maxsize=None)
|
| 275 |
+
def is_satisfied_by(requirement: Requirement, candidate: Candidate) -> bool:
|
| 276 |
+
return requirement.is_satisfied_by(candidate)
|
| 277 |
+
|
| 278 |
+
def get_dependencies(self, candidate: Candidate) -> Iterable[Requirement]:
|
| 279 |
+
with_requires = not self._ignore_dependencies
|
| 280 |
+
# iter_dependencies() can perform nontrivial work so delay until needed.
|
| 281 |
+
return (r for r in candidate.iter_dependencies(with_requires) if r is not None)
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/reporter.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import defaultdict
|
| 2 |
+
from logging import getLogger
|
| 3 |
+
from typing import Any, DefaultDict, Optional
|
| 4 |
+
|
| 5 |
+
from pip._vendor.resolvelib.reporters import BaseReporter
|
| 6 |
+
|
| 7 |
+
from .base import Candidate, Requirement
|
| 8 |
+
|
| 9 |
+
logger = getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PipReporter(BaseReporter[Requirement, Candidate, str]):
|
| 13 |
+
def __init__(self) -> None:
|
| 14 |
+
self.reject_count_by_package: DefaultDict[str, int] = defaultdict(int)
|
| 15 |
+
|
| 16 |
+
self._messages_at_reject_count = {
|
| 17 |
+
1: (
|
| 18 |
+
"pip is looking at multiple versions of {package_name} to "
|
| 19 |
+
"determine which version is compatible with other "
|
| 20 |
+
"requirements. This could take a while."
|
| 21 |
+
),
|
| 22 |
+
8: (
|
| 23 |
+
"pip is still looking at multiple versions of {package_name} to "
|
| 24 |
+
"determine which version is compatible with other "
|
| 25 |
+
"requirements. This could take a while."
|
| 26 |
+
),
|
| 27 |
+
13: (
|
| 28 |
+
"This is taking longer than usual. You might need to provide "
|
| 29 |
+
"the dependency resolver with stricter constraints to reduce "
|
| 30 |
+
"runtime. See https://pip.pypa.io/warnings/backtracking for "
|
| 31 |
+
"guidance. If you want to abort this run, press Ctrl + C."
|
| 32 |
+
),
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
|
| 36 |
+
self.reject_count_by_package[candidate.name] += 1
|
| 37 |
+
|
| 38 |
+
count = self.reject_count_by_package[candidate.name]
|
| 39 |
+
if count not in self._messages_at_reject_count:
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
message = self._messages_at_reject_count[count]
|
| 43 |
+
logger.info("INFO: %s", message.format(package_name=candidate.name))
|
| 44 |
+
|
| 45 |
+
msg = "Will try a different candidate, due to conflict:"
|
| 46 |
+
for req_info in criterion.information:
|
| 47 |
+
req, parent = req_info.requirement, req_info.parent
|
| 48 |
+
# Inspired by Factory.get_installation_error
|
| 49 |
+
msg += "\n "
|
| 50 |
+
if parent:
|
| 51 |
+
msg += f"{parent.name} {parent.version} depends on "
|
| 52 |
+
else:
|
| 53 |
+
msg += "The user requested "
|
| 54 |
+
msg += req.format_for_error()
|
| 55 |
+
logger.debug(msg)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class PipDebuggingReporter(BaseReporter[Requirement, Candidate, str]):
|
| 59 |
+
"""A reporter that does an info log for every event it sees."""
|
| 60 |
+
|
| 61 |
+
def starting(self) -> None:
|
| 62 |
+
logger.info("Reporter.starting()")
|
| 63 |
+
|
| 64 |
+
def starting_round(self, index: int) -> None:
|
| 65 |
+
logger.info("Reporter.starting_round(%r)", index)
|
| 66 |
+
|
| 67 |
+
def ending_round(self, index: int, state: Any) -> None:
|
| 68 |
+
logger.info("Reporter.ending_round(%r, state)", index)
|
| 69 |
+
logger.debug("Reporter.ending_round(%r, %r)", index, state)
|
| 70 |
+
|
| 71 |
+
def ending(self, state: Any) -> None:
|
| 72 |
+
logger.info("Reporter.ending(%r)", state)
|
| 73 |
+
|
| 74 |
+
def adding_requirement(
|
| 75 |
+
self, requirement: Requirement, parent: Optional[Candidate]
|
| 76 |
+
) -> None:
|
| 77 |
+
logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent)
|
| 78 |
+
|
| 79 |
+
def rejecting_candidate(self, criterion: Any, candidate: Candidate) -> None:
|
| 80 |
+
logger.info("Reporter.rejecting_candidate(%r, %r)", criterion, candidate)
|
| 81 |
+
|
| 82 |
+
def pinning(self, candidate: Candidate) -> None:
|
| 83 |
+
logger.info("Reporter.pinning(%r)", candidate)
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/requirements.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Any, Optional
|
| 2 |
+
|
| 3 |
+
from pip._vendor.packaging.specifiers import SpecifierSet
|
| 4 |
+
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
| 5 |
+
|
| 6 |
+
from pip._internal.req.constructors import install_req_drop_extras
|
| 7 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 8 |
+
|
| 9 |
+
from .base import Candidate, CandidateLookup, Requirement, format_name
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ExplicitRequirement(Requirement):
|
| 13 |
+
def __init__(self, candidate: Candidate) -> None:
|
| 14 |
+
self.candidate = candidate
|
| 15 |
+
|
| 16 |
+
def __str__(self) -> str:
|
| 17 |
+
return str(self.candidate)
|
| 18 |
+
|
| 19 |
+
def __repr__(self) -> str:
|
| 20 |
+
return f"{self.__class__.__name__}({self.candidate!r})"
|
| 21 |
+
|
| 22 |
+
def __hash__(self) -> int:
|
| 23 |
+
return hash(self.candidate)
|
| 24 |
+
|
| 25 |
+
def __eq__(self, other: Any) -> bool:
|
| 26 |
+
if not isinstance(other, ExplicitRequirement):
|
| 27 |
+
return False
|
| 28 |
+
return self.candidate == other.candidate
|
| 29 |
+
|
| 30 |
+
@property
|
| 31 |
+
def project_name(self) -> NormalizedName:
|
| 32 |
+
# No need to canonicalize - the candidate did this
|
| 33 |
+
return self.candidate.project_name
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def name(self) -> str:
|
| 37 |
+
# No need to canonicalize - the candidate did this
|
| 38 |
+
return self.candidate.name
|
| 39 |
+
|
| 40 |
+
def format_for_error(self) -> str:
|
| 41 |
+
return self.candidate.format_for_error()
|
| 42 |
+
|
| 43 |
+
def get_candidate_lookup(self) -> CandidateLookup:
|
| 44 |
+
return self.candidate, None
|
| 45 |
+
|
| 46 |
+
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
| 47 |
+
return candidate == self.candidate
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class SpecifierRequirement(Requirement):
|
| 51 |
+
def __init__(self, ireq: InstallRequirement) -> None:
|
| 52 |
+
assert ireq.link is None, "This is a link, not a specifier"
|
| 53 |
+
self._ireq = ireq
|
| 54 |
+
self._equal_cache: Optional[str] = None
|
| 55 |
+
self._hash: Optional[int] = None
|
| 56 |
+
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
|
| 57 |
+
|
| 58 |
+
@property
|
| 59 |
+
def _equal(self) -> str:
|
| 60 |
+
if self._equal_cache is not None:
|
| 61 |
+
return self._equal_cache
|
| 62 |
+
|
| 63 |
+
self._equal_cache = str(self._ireq)
|
| 64 |
+
return self._equal_cache
|
| 65 |
+
|
| 66 |
+
def __str__(self) -> str:
|
| 67 |
+
return str(self._ireq.req)
|
| 68 |
+
|
| 69 |
+
def __repr__(self) -> str:
|
| 70 |
+
return f"{self.__class__.__name__}({str(self._ireq.req)!r})"
|
| 71 |
+
|
| 72 |
+
def __eq__(self, other: object) -> bool:
|
| 73 |
+
if not isinstance(other, SpecifierRequirement):
|
| 74 |
+
return NotImplemented
|
| 75 |
+
return self._equal == other._equal
|
| 76 |
+
|
| 77 |
+
def __hash__(self) -> int:
|
| 78 |
+
if self._hash is not None:
|
| 79 |
+
return self._hash
|
| 80 |
+
|
| 81 |
+
self._hash = hash(self._equal)
|
| 82 |
+
return self._hash
|
| 83 |
+
|
| 84 |
+
@property
|
| 85 |
+
def project_name(self) -> NormalizedName:
|
| 86 |
+
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
|
| 87 |
+
return canonicalize_name(self._ireq.req.name)
|
| 88 |
+
|
| 89 |
+
@property
|
| 90 |
+
def name(self) -> str:
|
| 91 |
+
return format_name(self.project_name, self._extras)
|
| 92 |
+
|
| 93 |
+
def format_for_error(self) -> str:
|
| 94 |
+
# Convert comma-separated specifiers into "A, B, ..., F and G"
|
| 95 |
+
# This makes the specifier a bit more "human readable", without
|
| 96 |
+
# risking a change in meaning. (Hopefully! Not all edge cases have
|
| 97 |
+
# been checked)
|
| 98 |
+
parts = [s.strip() for s in str(self).split(",")]
|
| 99 |
+
if len(parts) == 0:
|
| 100 |
+
return ""
|
| 101 |
+
elif len(parts) == 1:
|
| 102 |
+
return parts[0]
|
| 103 |
+
|
| 104 |
+
return ", ".join(parts[:-1]) + " and " + parts[-1]
|
| 105 |
+
|
| 106 |
+
def get_candidate_lookup(self) -> CandidateLookup:
|
| 107 |
+
return None, self._ireq
|
| 108 |
+
|
| 109 |
+
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
| 110 |
+
assert candidate.name == self.name, (
|
| 111 |
+
f"Internal issue: Candidate is not for this requirement "
|
| 112 |
+
f"{candidate.name} vs {self.name}"
|
| 113 |
+
)
|
| 114 |
+
# We can safely always allow prereleases here since PackageFinder
|
| 115 |
+
# already implements the prerelease logic, and would have filtered out
|
| 116 |
+
# prerelease candidates if the user does not expect them.
|
| 117 |
+
assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
|
| 118 |
+
spec = self._ireq.req.specifier
|
| 119 |
+
return spec.contains(candidate.version, prereleases=True)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class SpecifierWithoutExtrasRequirement(SpecifierRequirement):
|
| 123 |
+
"""
|
| 124 |
+
Requirement backed by an install requirement on a base package.
|
| 125 |
+
Trims extras from its install requirement if there are any.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
def __init__(self, ireq: InstallRequirement) -> None:
|
| 129 |
+
assert ireq.link is None, "This is a link, not a specifier"
|
| 130 |
+
self._ireq = install_req_drop_extras(ireq)
|
| 131 |
+
self._equal_cache: Optional[str] = None
|
| 132 |
+
self._hash: Optional[int] = None
|
| 133 |
+
self._extras = frozenset(canonicalize_name(e) for e in self._ireq.extras)
|
| 134 |
+
|
| 135 |
+
@property
|
| 136 |
+
def _equal(self) -> str:
|
| 137 |
+
if self._equal_cache is not None:
|
| 138 |
+
return self._equal_cache
|
| 139 |
+
|
| 140 |
+
self._equal_cache = str(self._ireq)
|
| 141 |
+
return self._equal_cache
|
| 142 |
+
|
| 143 |
+
def __eq__(self, other: object) -> bool:
|
| 144 |
+
if not isinstance(other, SpecifierWithoutExtrasRequirement):
|
| 145 |
+
return NotImplemented
|
| 146 |
+
return self._equal == other._equal
|
| 147 |
+
|
| 148 |
+
def __hash__(self) -> int:
|
| 149 |
+
if self._hash is not None:
|
| 150 |
+
return self._hash
|
| 151 |
+
|
| 152 |
+
self._hash = hash(self._equal)
|
| 153 |
+
return self._hash
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class RequiresPythonRequirement(Requirement):
|
| 157 |
+
"""A requirement representing Requires-Python metadata."""
|
| 158 |
+
|
| 159 |
+
def __init__(self, specifier: SpecifierSet, match: Candidate) -> None:
|
| 160 |
+
self.specifier = specifier
|
| 161 |
+
self._specifier_string = str(specifier) # for faster __eq__
|
| 162 |
+
self._hash: Optional[int] = None
|
| 163 |
+
self._candidate = match
|
| 164 |
+
|
| 165 |
+
def __str__(self) -> str:
|
| 166 |
+
return f"Python {self.specifier}"
|
| 167 |
+
|
| 168 |
+
def __repr__(self) -> str:
|
| 169 |
+
return f"{self.__class__.__name__}({str(self.specifier)!r})"
|
| 170 |
+
|
| 171 |
+
def __hash__(self) -> int:
|
| 172 |
+
if self._hash is not None:
|
| 173 |
+
return self._hash
|
| 174 |
+
|
| 175 |
+
self._hash = hash((self._specifier_string, self._candidate))
|
| 176 |
+
return self._hash
|
| 177 |
+
|
| 178 |
+
def __eq__(self, other: Any) -> bool:
|
| 179 |
+
if not isinstance(other, RequiresPythonRequirement):
|
| 180 |
+
return False
|
| 181 |
+
return (
|
| 182 |
+
self._specifier_string == other._specifier_string
|
| 183 |
+
and self._candidate == other._candidate
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
@property
|
| 187 |
+
def project_name(self) -> NormalizedName:
|
| 188 |
+
return self._candidate.project_name
|
| 189 |
+
|
| 190 |
+
@property
|
| 191 |
+
def name(self) -> str:
|
| 192 |
+
return self._candidate.name
|
| 193 |
+
|
| 194 |
+
def format_for_error(self) -> str:
|
| 195 |
+
return str(self)
|
| 196 |
+
|
| 197 |
+
def get_candidate_lookup(self) -> CandidateLookup:
|
| 198 |
+
if self.specifier.contains(self._candidate.version, prereleases=True):
|
| 199 |
+
return self._candidate, None
|
| 200 |
+
return None, None
|
| 201 |
+
|
| 202 |
+
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
| 203 |
+
assert candidate.name == self._candidate.name, "Not Python candidate"
|
| 204 |
+
# We can safely always allow prereleases here since PackageFinder
|
| 205 |
+
# already implements the prerelease logic, and would have filtered out
|
| 206 |
+
# prerelease candidates if the user does not expect them.
|
| 207 |
+
return self.specifier.contains(candidate.version, prereleases=True)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
class UnsatisfiableRequirement(Requirement):
|
| 211 |
+
"""A requirement that cannot be satisfied."""
|
| 212 |
+
|
| 213 |
+
def __init__(self, name: NormalizedName) -> None:
|
| 214 |
+
self._name = name
|
| 215 |
+
|
| 216 |
+
def __str__(self) -> str:
|
| 217 |
+
return f"{self._name} (unavailable)"
|
| 218 |
+
|
| 219 |
+
def __repr__(self) -> str:
|
| 220 |
+
return f"{self.__class__.__name__}({str(self._name)!r})"
|
| 221 |
+
|
| 222 |
+
def __eq__(self, other: object) -> bool:
|
| 223 |
+
if not isinstance(other, UnsatisfiableRequirement):
|
| 224 |
+
return NotImplemented
|
| 225 |
+
return self._name == other._name
|
| 226 |
+
|
| 227 |
+
def __hash__(self) -> int:
|
| 228 |
+
return hash(self._name)
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def project_name(self) -> NormalizedName:
|
| 232 |
+
return self._name
|
| 233 |
+
|
| 234 |
+
@property
|
| 235 |
+
def name(self) -> str:
|
| 236 |
+
return self._name
|
| 237 |
+
|
| 238 |
+
def format_for_error(self) -> str:
|
| 239 |
+
return str(self)
|
| 240 |
+
|
| 241 |
+
def get_candidate_lookup(self) -> CandidateLookup:
|
| 242 |
+
return None, None
|
| 243 |
+
|
| 244 |
+
def is_satisfied_by(self, candidate: Candidate) -> bool:
|
| 245 |
+
return False
|
.env/lib/python3.13/site-packages/pip/_internal/resolution/resolvelib/resolver.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import functools
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast
|
| 6 |
+
|
| 7 |
+
from pip._vendor.packaging.utils import canonicalize_name
|
| 8 |
+
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible, ResolutionTooDeep
|
| 9 |
+
from pip._vendor.resolvelib import Resolver as RLResolver
|
| 10 |
+
from pip._vendor.resolvelib.structs import DirectedGraph
|
| 11 |
+
|
| 12 |
+
from pip._internal.cache import WheelCache
|
| 13 |
+
from pip._internal.exceptions import ResolutionTooDeepError
|
| 14 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 15 |
+
from pip._internal.operations.prepare import RequirementPreparer
|
| 16 |
+
from pip._internal.req.constructors import install_req_extend_extras
|
| 17 |
+
from pip._internal.req.req_install import InstallRequirement
|
| 18 |
+
from pip._internal.req.req_set import RequirementSet
|
| 19 |
+
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
|
| 20 |
+
from pip._internal.resolution.resolvelib.provider import PipProvider
|
| 21 |
+
from pip._internal.resolution.resolvelib.reporter import (
|
| 22 |
+
PipDebuggingReporter,
|
| 23 |
+
PipReporter,
|
| 24 |
+
)
|
| 25 |
+
from pip._internal.utils.packaging import get_requirement
|
| 26 |
+
|
| 27 |
+
from .base import Candidate, Requirement
|
| 28 |
+
from .factory import Factory
|
| 29 |
+
|
| 30 |
+
if TYPE_CHECKING:
|
| 31 |
+
from pip._vendor.resolvelib.resolvers import Result as RLResult
|
| 32 |
+
|
| 33 |
+
Result = RLResult[Requirement, Candidate, str]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
logger = logging.getLogger(__name__)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Resolver(BaseResolver):
|
| 40 |
+
_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}
|
| 41 |
+
|
| 42 |
+
def __init__(
|
| 43 |
+
self,
|
| 44 |
+
preparer: RequirementPreparer,
|
| 45 |
+
finder: PackageFinder,
|
| 46 |
+
wheel_cache: Optional[WheelCache],
|
| 47 |
+
make_install_req: InstallRequirementProvider,
|
| 48 |
+
use_user_site: bool,
|
| 49 |
+
ignore_dependencies: bool,
|
| 50 |
+
ignore_installed: bool,
|
| 51 |
+
ignore_requires_python: bool,
|
| 52 |
+
force_reinstall: bool,
|
| 53 |
+
upgrade_strategy: str,
|
| 54 |
+
py_version_info: Optional[Tuple[int, ...]] = None,
|
| 55 |
+
):
|
| 56 |
+
super().__init__()
|
| 57 |
+
assert upgrade_strategy in self._allowed_strategies
|
| 58 |
+
|
| 59 |
+
self.factory = Factory(
|
| 60 |
+
finder=finder,
|
| 61 |
+
preparer=preparer,
|
| 62 |
+
make_install_req=make_install_req,
|
| 63 |
+
wheel_cache=wheel_cache,
|
| 64 |
+
use_user_site=use_user_site,
|
| 65 |
+
force_reinstall=force_reinstall,
|
| 66 |
+
ignore_installed=ignore_installed,
|
| 67 |
+
ignore_requires_python=ignore_requires_python,
|
| 68 |
+
py_version_info=py_version_info,
|
| 69 |
+
)
|
| 70 |
+
self.ignore_dependencies = ignore_dependencies
|
| 71 |
+
self.upgrade_strategy = upgrade_strategy
|
| 72 |
+
self._result: Optional[Result] = None
|
| 73 |
+
|
| 74 |
+
def resolve(
|
| 75 |
+
self, root_reqs: List[InstallRequirement], check_supported_wheels: bool
|
| 76 |
+
) -> RequirementSet:
|
| 77 |
+
collected = self.factory.collect_root_requirements(root_reqs)
|
| 78 |
+
provider = PipProvider(
|
| 79 |
+
factory=self.factory,
|
| 80 |
+
constraints=collected.constraints,
|
| 81 |
+
ignore_dependencies=self.ignore_dependencies,
|
| 82 |
+
upgrade_strategy=self.upgrade_strategy,
|
| 83 |
+
user_requested=collected.user_requested,
|
| 84 |
+
)
|
| 85 |
+
if "PIP_RESOLVER_DEBUG" in os.environ:
|
| 86 |
+
reporter: BaseReporter[Requirement, Candidate, str] = PipDebuggingReporter()
|
| 87 |
+
else:
|
| 88 |
+
reporter = PipReporter()
|
| 89 |
+
resolver: RLResolver[Requirement, Candidate, str] = RLResolver(
|
| 90 |
+
provider,
|
| 91 |
+
reporter,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
limit_how_complex_resolution_can_be = 200000
|
| 96 |
+
result = self._result = resolver.resolve(
|
| 97 |
+
collected.requirements, max_rounds=limit_how_complex_resolution_can_be
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
except ResolutionImpossible as e:
|
| 101 |
+
error = self.factory.get_installation_error(
|
| 102 |
+
cast("ResolutionImpossible[Requirement, Candidate]", e),
|
| 103 |
+
collected.constraints,
|
| 104 |
+
)
|
| 105 |
+
raise error from e
|
| 106 |
+
except ResolutionTooDeep:
|
| 107 |
+
raise ResolutionTooDeepError from None
|
| 108 |
+
|
| 109 |
+
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
|
| 110 |
+
# process candidates with extras last to ensure their base equivalent is
|
| 111 |
+
# already in the req_set if appropriate.
|
| 112 |
+
# Python's sort is stable so using a binary key function keeps relative order
|
| 113 |
+
# within both subsets.
|
| 114 |
+
for candidate in sorted(
|
| 115 |
+
result.mapping.values(), key=lambda c: c.name != c.project_name
|
| 116 |
+
):
|
| 117 |
+
ireq = candidate.get_install_requirement()
|
| 118 |
+
if ireq is None:
|
| 119 |
+
if candidate.name != candidate.project_name:
|
| 120 |
+
# extend existing req's extras
|
| 121 |
+
with contextlib.suppress(KeyError):
|
| 122 |
+
req = req_set.get_requirement(candidate.project_name)
|
| 123 |
+
req_set.add_named_requirement(
|
| 124 |
+
install_req_extend_extras(
|
| 125 |
+
req, get_requirement(candidate.name).extras
|
| 126 |
+
)
|
| 127 |
+
)
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
# Check if there is already an installation under the same name,
|
| 131 |
+
# and set a flag for later stages to uninstall it, if needed.
|
| 132 |
+
installed_dist = self.factory.get_dist_to_uninstall(candidate)
|
| 133 |
+
if installed_dist is None:
|
| 134 |
+
# There is no existing installation -- nothing to uninstall.
|
| 135 |
+
ireq.should_reinstall = False
|
| 136 |
+
elif self.factory.force_reinstall:
|
| 137 |
+
# The --force-reinstall flag is set -- reinstall.
|
| 138 |
+
ireq.should_reinstall = True
|
| 139 |
+
elif installed_dist.version != candidate.version:
|
| 140 |
+
# The installation is different in version -- reinstall.
|
| 141 |
+
ireq.should_reinstall = True
|
| 142 |
+
elif candidate.is_editable or installed_dist.editable:
|
| 143 |
+
# The incoming distribution is editable, or different in
|
| 144 |
+
# editable-ness to installation -- reinstall.
|
| 145 |
+
ireq.should_reinstall = True
|
| 146 |
+
elif candidate.source_link and candidate.source_link.is_file:
|
| 147 |
+
# The incoming distribution is under file://
|
| 148 |
+
if candidate.source_link.is_wheel:
|
| 149 |
+
# is a local wheel -- do nothing.
|
| 150 |
+
logger.info(
|
| 151 |
+
"%s is already installed with the same version as the "
|
| 152 |
+
"provided wheel. Use --force-reinstall to force an "
|
| 153 |
+
"installation of the wheel.",
|
| 154 |
+
ireq.name,
|
| 155 |
+
)
|
| 156 |
+
continue
|
| 157 |
+
|
| 158 |
+
# is a local sdist or path -- reinstall
|
| 159 |
+
ireq.should_reinstall = True
|
| 160 |
+
else:
|
| 161 |
+
continue
|
| 162 |
+
|
| 163 |
+
link = candidate.source_link
|
| 164 |
+
if link and link.is_yanked:
|
| 165 |
+
# The reason can contain non-ASCII characters, Unicode
|
| 166 |
+
# is required for Python 2.
|
| 167 |
+
msg = (
|
| 168 |
+
"The candidate selected for download or install is a "
|
| 169 |
+
"yanked version: {name!r} candidate (version {version} "
|
| 170 |
+
"at {link})\nReason for being yanked: {reason}"
|
| 171 |
+
).format(
|
| 172 |
+
name=candidate.name,
|
| 173 |
+
version=candidate.version,
|
| 174 |
+
link=link,
|
| 175 |
+
reason=link.yanked_reason or "<none given>",
|
| 176 |
+
)
|
| 177 |
+
logger.warning(msg)
|
| 178 |
+
|
| 179 |
+
req_set.add_named_requirement(ireq)
|
| 180 |
+
|
| 181 |
+
reqs = req_set.all_requirements
|
| 182 |
+
self.factory.preparer.prepare_linked_requirements_more(reqs)
|
| 183 |
+
for req in reqs:
|
| 184 |
+
req.prepared = True
|
| 185 |
+
req.needs_more_preparation = False
|
| 186 |
+
return req_set
|
| 187 |
+
|
| 188 |
+
def get_installation_order(
|
| 189 |
+
self, req_set: RequirementSet
|
| 190 |
+
) -> List[InstallRequirement]:
|
| 191 |
+
"""Get order for installation of requirements in RequirementSet.
|
| 192 |
+
|
| 193 |
+
The returned list contains a requirement before another that depends on
|
| 194 |
+
it. This helps ensure that the environment is kept consistent as they
|
| 195 |
+
get installed one-by-one.
|
| 196 |
+
|
| 197 |
+
The current implementation creates a topological ordering of the
|
| 198 |
+
dependency graph, giving more weight to packages with less
|
| 199 |
+
or no dependencies, while breaking any cycles in the graph at
|
| 200 |
+
arbitrary points. We make no guarantees about where the cycle
|
| 201 |
+
would be broken, other than it *would* be broken.
|
| 202 |
+
"""
|
| 203 |
+
assert self._result is not None, "must call resolve() first"
|
| 204 |
+
|
| 205 |
+
if not req_set.requirements:
|
| 206 |
+
# Nothing is left to install, so we do not need an order.
|
| 207 |
+
return []
|
| 208 |
+
|
| 209 |
+
graph = self._result.graph
|
| 210 |
+
weights = get_topological_weights(graph, set(req_set.requirements.keys()))
|
| 211 |
+
|
| 212 |
+
sorted_items = sorted(
|
| 213 |
+
req_set.requirements.items(),
|
| 214 |
+
key=functools.partial(_req_set_item_sorter, weights=weights),
|
| 215 |
+
reverse=True,
|
| 216 |
+
)
|
| 217 |
+
return [ireq for _, ireq in sorted_items]
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def get_topological_weights(
|
| 221 |
+
graph: "DirectedGraph[Optional[str]]", requirement_keys: Set[str]
|
| 222 |
+
) -> Dict[Optional[str], int]:
|
| 223 |
+
"""Assign weights to each node based on how "deep" they are.
|
| 224 |
+
|
| 225 |
+
This implementation may change at any point in the future without prior
|
| 226 |
+
notice.
|
| 227 |
+
|
| 228 |
+
We first simplify the dependency graph by pruning any leaves and giving them
|
| 229 |
+
the highest weight: a package without any dependencies should be installed
|
| 230 |
+
first. This is done again and again in the same way, giving ever less weight
|
| 231 |
+
to the newly found leaves. The loop stops when no leaves are left: all
|
| 232 |
+
remaining packages have at least one dependency left in the graph.
|
| 233 |
+
|
| 234 |
+
Then we continue with the remaining graph, by taking the length for the
|
| 235 |
+
longest path to any node from root, ignoring any paths that contain a single
|
| 236 |
+
node twice (i.e. cycles). This is done through a depth-first search through
|
| 237 |
+
the graph, while keeping track of the path to the node.
|
| 238 |
+
|
| 239 |
+
Cycles in the graph result would result in node being revisited while also
|
| 240 |
+
being on its own path. In this case, take no action. This helps ensure we
|
| 241 |
+
don't get stuck in a cycle.
|
| 242 |
+
|
| 243 |
+
When assigning weight, the longer path (i.e. larger length) is preferred.
|
| 244 |
+
|
| 245 |
+
We are only interested in the weights of packages that are in the
|
| 246 |
+
requirement_keys.
|
| 247 |
+
"""
|
| 248 |
+
path: Set[Optional[str]] = set()
|
| 249 |
+
weights: Dict[Optional[str], int] = {}
|
| 250 |
+
|
| 251 |
+
def visit(node: Optional[str]) -> None:
|
| 252 |
+
if node in path:
|
| 253 |
+
# We hit a cycle, so we'll break it here.
|
| 254 |
+
return
|
| 255 |
+
|
| 256 |
+
# Time to visit the children!
|
| 257 |
+
path.add(node)
|
| 258 |
+
for child in graph.iter_children(node):
|
| 259 |
+
visit(child)
|
| 260 |
+
path.remove(node)
|
| 261 |
+
|
| 262 |
+
if node not in requirement_keys:
|
| 263 |
+
return
|
| 264 |
+
|
| 265 |
+
last_known_parent_count = weights.get(node, 0)
|
| 266 |
+
weights[node] = max(last_known_parent_count, len(path))
|
| 267 |
+
|
| 268 |
+
# Simplify the graph, pruning leaves that have no dependencies.
|
| 269 |
+
# This is needed for large graphs (say over 200 packages) because the
|
| 270 |
+
# `visit` function is exponentially slower then, taking minutes.
|
| 271 |
+
# See https://github.com/pypa/pip/issues/10557
|
| 272 |
+
# We will loop until we explicitly break the loop.
|
| 273 |
+
while True:
|
| 274 |
+
leaves = set()
|
| 275 |
+
for key in graph:
|
| 276 |
+
if key is None:
|
| 277 |
+
continue
|
| 278 |
+
for _child in graph.iter_children(key):
|
| 279 |
+
# This means we have at least one child
|
| 280 |
+
break
|
| 281 |
+
else:
|
| 282 |
+
# No child.
|
| 283 |
+
leaves.add(key)
|
| 284 |
+
if not leaves:
|
| 285 |
+
# We are done simplifying.
|
| 286 |
+
break
|
| 287 |
+
# Calculate the weight for the leaves.
|
| 288 |
+
weight = len(graph) - 1
|
| 289 |
+
for leaf in leaves:
|
| 290 |
+
if leaf not in requirement_keys:
|
| 291 |
+
continue
|
| 292 |
+
weights[leaf] = weight
|
| 293 |
+
# Remove the leaves from the graph, making it simpler.
|
| 294 |
+
for leaf in leaves:
|
| 295 |
+
graph.remove(leaf)
|
| 296 |
+
|
| 297 |
+
# Visit the remaining graph.
|
| 298 |
+
# `None` is guaranteed to be the root node by resolvelib.
|
| 299 |
+
visit(None)
|
| 300 |
+
|
| 301 |
+
# Sanity check: all requirement keys should be in the weights,
|
| 302 |
+
# and no other keys should be in the weights.
|
| 303 |
+
difference = set(weights.keys()).difference(requirement_keys)
|
| 304 |
+
assert not difference, difference
|
| 305 |
+
|
| 306 |
+
return weights
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def _req_set_item_sorter(
|
| 310 |
+
item: Tuple[str, InstallRequirement],
|
| 311 |
+
weights: Dict[Optional[str], int],
|
| 312 |
+
) -> Tuple[int, str]:
|
| 313 |
+
"""Key function used to sort install requirements for installation.
|
| 314 |
+
|
| 315 |
+
Based on the "weight" mapping calculated in ``get_installation_order()``.
|
| 316 |
+
The canonical package name is returned as the second member as a tie-
|
| 317 |
+
breaker to ensure the result is predictable, which is useful in tests.
|
| 318 |
+
"""
|
| 319 |
+
name = canonicalize_name(item[0])
|
| 320 |
+
return weights[name], name
|
.env/lib/python3.13/site-packages/pip/_internal/self_outdated_check.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import functools
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import logging
|
| 6 |
+
import optparse
|
| 7 |
+
import os.path
|
| 8 |
+
import sys
|
| 9 |
+
from dataclasses import dataclass
|
| 10 |
+
from typing import Any, Callable, Dict, Optional
|
| 11 |
+
|
| 12 |
+
from pip._vendor.packaging.version import Version
|
| 13 |
+
from pip._vendor.packaging.version import parse as parse_version
|
| 14 |
+
from pip._vendor.rich.console import Group
|
| 15 |
+
from pip._vendor.rich.markup import escape
|
| 16 |
+
from pip._vendor.rich.text import Text
|
| 17 |
+
|
| 18 |
+
from pip._internal.index.collector import LinkCollector
|
| 19 |
+
from pip._internal.index.package_finder import PackageFinder
|
| 20 |
+
from pip._internal.metadata import get_default_environment
|
| 21 |
+
from pip._internal.models.selection_prefs import SelectionPreferences
|
| 22 |
+
from pip._internal.network.session import PipSession
|
| 23 |
+
from pip._internal.utils.compat import WINDOWS
|
| 24 |
+
from pip._internal.utils.entrypoints import (
|
| 25 |
+
get_best_invocation_for_this_pip,
|
| 26 |
+
get_best_invocation_for_this_python,
|
| 27 |
+
)
|
| 28 |
+
from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace
|
| 29 |
+
from pip._internal.utils.misc import (
|
| 30 |
+
ExternallyManagedEnvironment,
|
| 31 |
+
check_externally_managed,
|
| 32 |
+
ensure_dir,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
_WEEK = datetime.timedelta(days=7)
|
| 36 |
+
|
| 37 |
+
logger = logging.getLogger(__name__)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _get_statefile_name(key: str) -> str:
|
| 41 |
+
key_bytes = key.encode()
|
| 42 |
+
name = hashlib.sha224(key_bytes).hexdigest()
|
| 43 |
+
return name
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _convert_date(isodate: str) -> datetime.datetime:
|
| 47 |
+
"""Convert an ISO format string to a date.
|
| 48 |
+
|
| 49 |
+
Handles the format 2020-01-22T14:24:01Z (trailing Z)
|
| 50 |
+
which is not supported by older versions of fromisoformat.
|
| 51 |
+
"""
|
| 52 |
+
return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class SelfCheckState:
|
| 56 |
+
def __init__(self, cache_dir: str) -> None:
|
| 57 |
+
self._state: Dict[str, Any] = {}
|
| 58 |
+
self._statefile_path = None
|
| 59 |
+
|
| 60 |
+
# Try to load the existing state
|
| 61 |
+
if cache_dir:
|
| 62 |
+
self._statefile_path = os.path.join(
|
| 63 |
+
cache_dir, "selfcheck", _get_statefile_name(self.key)
|
| 64 |
+
)
|
| 65 |
+
try:
|
| 66 |
+
with open(self._statefile_path, encoding="utf-8") as statefile:
|
| 67 |
+
self._state = json.load(statefile)
|
| 68 |
+
except (OSError, ValueError, KeyError):
|
| 69 |
+
# Explicitly suppressing exceptions, since we don't want to
|
| 70 |
+
# error out if the cache file is invalid.
|
| 71 |
+
pass
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def key(self) -> str:
|
| 75 |
+
return sys.prefix
|
| 76 |
+
|
| 77 |
+
def get(self, current_time: datetime.datetime) -> Optional[str]:
|
| 78 |
+
"""Check if we have a not-outdated version loaded already."""
|
| 79 |
+
if not self._state:
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
if "last_check" not in self._state:
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
if "pypi_version" not in self._state:
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
# Determine if we need to refresh the state
|
| 89 |
+
last_check = _convert_date(self._state["last_check"])
|
| 90 |
+
time_since_last_check = current_time - last_check
|
| 91 |
+
if time_since_last_check > _WEEK:
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
return self._state["pypi_version"]
|
| 95 |
+
|
| 96 |
+
def set(self, pypi_version: str, current_time: datetime.datetime) -> None:
|
| 97 |
+
# If we do not have a path to cache in, don't bother saving.
|
| 98 |
+
if not self._statefile_path:
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
# Check to make sure that we own the directory
|
| 102 |
+
if not check_path_owner(os.path.dirname(self._statefile_path)):
|
| 103 |
+
return
|
| 104 |
+
|
| 105 |
+
# Now that we've ensured the directory is owned by this user, we'll go
|
| 106 |
+
# ahead and make sure that all our directories are created.
|
| 107 |
+
ensure_dir(os.path.dirname(self._statefile_path))
|
| 108 |
+
|
| 109 |
+
state = {
|
| 110 |
+
# Include the key so it's easy to tell which pip wrote the
|
| 111 |
+
# file.
|
| 112 |
+
"key": self.key,
|
| 113 |
+
"last_check": current_time.isoformat(),
|
| 114 |
+
"pypi_version": pypi_version,
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
text = json.dumps(state, sort_keys=True, separators=(",", ":"))
|
| 118 |
+
|
| 119 |
+
with adjacent_tmp_file(self._statefile_path) as f:
|
| 120 |
+
f.write(text.encode())
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
# Since we have a prefix-specific state file, we can just
|
| 124 |
+
# overwrite whatever is there, no need to check.
|
| 125 |
+
replace(f.name, self._statefile_path)
|
| 126 |
+
except OSError:
|
| 127 |
+
# Best effort.
|
| 128 |
+
pass
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@dataclass
|
| 132 |
+
class UpgradePrompt:
|
| 133 |
+
old: str
|
| 134 |
+
new: str
|
| 135 |
+
|
| 136 |
+
def __rich__(self) -> Group:
|
| 137 |
+
if WINDOWS:
|
| 138 |
+
pip_cmd = f"{get_best_invocation_for_this_python()} -m pip"
|
| 139 |
+
else:
|
| 140 |
+
pip_cmd = get_best_invocation_for_this_pip()
|
| 141 |
+
|
| 142 |
+
notice = "[bold][[reset][blue]notice[reset][bold]][reset]"
|
| 143 |
+
return Group(
|
| 144 |
+
Text(),
|
| 145 |
+
Text.from_markup(
|
| 146 |
+
f"{notice} A new release of pip is available: "
|
| 147 |
+
f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
|
| 148 |
+
),
|
| 149 |
+
Text.from_markup(
|
| 150 |
+
f"{notice} To update, run: "
|
| 151 |
+
f"[green]{escape(pip_cmd)} install --upgrade pip"
|
| 152 |
+
),
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def was_installed_by_pip(pkg: str) -> bool:
|
| 157 |
+
"""Checks whether pkg was installed by pip
|
| 158 |
+
|
| 159 |
+
This is used not to display the upgrade message when pip is in fact
|
| 160 |
+
installed by system package manager, such as dnf on Fedora.
|
| 161 |
+
"""
|
| 162 |
+
dist = get_default_environment().get_distribution(pkg)
|
| 163 |
+
return dist is not None and "pip" == dist.installer
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _get_current_remote_pip_version(
|
| 167 |
+
session: PipSession, options: optparse.Values
|
| 168 |
+
) -> Optional[str]:
|
| 169 |
+
# Lets use PackageFinder to see what the latest pip version is
|
| 170 |
+
link_collector = LinkCollector.create(
|
| 171 |
+
session,
|
| 172 |
+
options=options,
|
| 173 |
+
suppress_no_index=True,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
# Pass allow_yanked=False so we don't suggest upgrading to a
|
| 177 |
+
# yanked version.
|
| 178 |
+
selection_prefs = SelectionPreferences(
|
| 179 |
+
allow_yanked=False,
|
| 180 |
+
allow_all_prereleases=False, # Explicitly set to False
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
finder = PackageFinder.create(
|
| 184 |
+
link_collector=link_collector,
|
| 185 |
+
selection_prefs=selection_prefs,
|
| 186 |
+
)
|
| 187 |
+
best_candidate = finder.find_best_candidate("pip").best_candidate
|
| 188 |
+
if best_candidate is None:
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
return str(best_candidate.version)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def _self_version_check_logic(
|
| 195 |
+
*,
|
| 196 |
+
state: SelfCheckState,
|
| 197 |
+
current_time: datetime.datetime,
|
| 198 |
+
local_version: Version,
|
| 199 |
+
get_remote_version: Callable[[], Optional[str]],
|
| 200 |
+
) -> Optional[UpgradePrompt]:
|
| 201 |
+
remote_version_str = state.get(current_time)
|
| 202 |
+
if remote_version_str is None:
|
| 203 |
+
remote_version_str = get_remote_version()
|
| 204 |
+
if remote_version_str is None:
|
| 205 |
+
logger.debug("No remote pip version found")
|
| 206 |
+
return None
|
| 207 |
+
state.set(remote_version_str, current_time)
|
| 208 |
+
|
| 209 |
+
remote_version = parse_version(remote_version_str)
|
| 210 |
+
logger.debug("Remote version of pip: %s", remote_version)
|
| 211 |
+
logger.debug("Local version of pip: %s", local_version)
|
| 212 |
+
|
| 213 |
+
pip_installed_by_pip = was_installed_by_pip("pip")
|
| 214 |
+
logger.debug("Was pip installed by pip? %s", pip_installed_by_pip)
|
| 215 |
+
if not pip_installed_by_pip:
|
| 216 |
+
return None # Only suggest upgrade if pip is installed by pip.
|
| 217 |
+
|
| 218 |
+
local_version_is_older = (
|
| 219 |
+
local_version < remote_version
|
| 220 |
+
and local_version.base_version != remote_version.base_version
|
| 221 |
+
)
|
| 222 |
+
if local_version_is_older:
|
| 223 |
+
return UpgradePrompt(old=str(local_version), new=remote_version_str)
|
| 224 |
+
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def pip_self_version_check(session: PipSession, options: optparse.Values) -> None:
|
| 229 |
+
"""Check for an update for pip.
|
| 230 |
+
|
| 231 |
+
Limit the frequency of checks to once per week. State is stored either in
|
| 232 |
+
the active virtualenv or in the user's USER_CACHE_DIR keyed off the prefix
|
| 233 |
+
of the pip script path.
|
| 234 |
+
"""
|
| 235 |
+
installed_dist = get_default_environment().get_distribution("pip")
|
| 236 |
+
if not installed_dist:
|
| 237 |
+
return
|
| 238 |
+
try:
|
| 239 |
+
check_externally_managed()
|
| 240 |
+
except ExternallyManagedEnvironment:
|
| 241 |
+
return
|
| 242 |
+
|
| 243 |
+
upgrade_prompt = _self_version_check_logic(
|
| 244 |
+
state=SelfCheckState(cache_dir=options.cache_dir),
|
| 245 |
+
current_time=datetime.datetime.now(datetime.timezone.utc),
|
| 246 |
+
local_version=installed_dist.version,
|
| 247 |
+
get_remote_version=functools.partial(
|
| 248 |
+
_get_current_remote_pip_version, session, options
|
| 249 |
+
),
|
| 250 |
+
)
|
| 251 |
+
if upgrade_prompt is not None:
|
| 252 |
+
logger.warning("%s", upgrade_prompt, extra={"rich": True})
|
.env/lib/python3.13/site-packages/pip/_internal/utils/__init__.py
ADDED
|
File without changes
|
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_jaraco_text.cpython-313.pyc
ADDED
|
Binary file (4.44 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/_log.cpython-313.pyc
ADDED
|
Binary file (1.92 kB). View file
|
|
|
.env/lib/python3.13/site-packages/pip/_internal/utils/__pycache__/appdirs.cpython-313.pyc
ADDED
|
Binary file (2.5 kB). View file
|
|
|