|
1 | 1 | import contextlib
|
| 2 | +import hashlib |
2 | 3 | import json
|
3 | 4 | import os
|
4 | 5 | import subprocess
|
5 | 6 | import sys
|
6 | 7 | import tempfile
|
| 8 | +import time |
7 | 9 | import warnings
|
8 | 10 | from pathlib import Path
|
9 | 11 | from typing import Any, Dict, List, Optional, Tuple, Union
|
|
24 | 26 | from pipenv.patched.pip._internal.req.req_install import InstallRequirement
|
25 | 27 | from pipenv.patched.pip._internal.utils.temp_dir import global_tempdir_manager
|
26 | 28 | from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
|
27 |
| -from pipenv.project import Project |
28 | 29 | from pipenv.utils import console, err
|
29 | 30 | from pipenv.utils.dependencies import determine_vcs_revision_hash, normalize_vcs_url
|
30 | 31 | from pipenv.utils.fileutils import create_tracked_tempdir
|
@@ -166,7 +167,7 @@ def check_if_package_req_skipped(
|
166 | 167 | def create(
|
167 | 168 | cls,
|
168 | 169 | deps: Dict[str, str],
|
169 |
| - project: Project, |
| 170 | + project, |
170 | 171 | index_lookup: Dict[str, str] = None,
|
171 | 172 | markers_lookup: Dict[str, str] = None,
|
172 | 173 | sources: List[str] = None,
|
@@ -707,6 +708,73 @@ def clean_results(self) -> List[Dict[str, Any]]:
|
707 | 708 | return list(results.values())
|
708 | 709 |
|
709 | 710 |
|
| 711 | +# Global cache for resolution results to avoid repeated expensive subprocess calls |
| 712 | +_resolution_cache = {} |
| 713 | +_resolution_cache_timestamp = {} |
| 714 | + |
| 715 | + |
| 716 | +def _generate_resolution_cache_key( |
| 717 | + deps, project, pipfile_category, pre, clear, allow_global, pypi_mirror, extra_pip_args |
| 718 | +): |
| 719 | + """Generate a cache key for resolution results.""" |
| 720 | + # Get lockfile and pipfile modification times |
| 721 | + lockfile_mtime = "no-lock" |
| 722 | + if project.lockfile_location: |
| 723 | + lockfile_path = Path(project.lockfile_location) |
| 724 | + if lockfile_path.exists(): |
| 725 | + lockfile_mtime = str(lockfile_path.stat().st_mtime) |
| 726 | + |
| 727 | + pipfile_mtime = "no-pipfile" |
| 728 | + if project.pipfile_location: |
| 729 | + pipfile_path = Path(project.pipfile_location) |
| 730 | + if pipfile_path.exists(): |
| 731 | + pipfile_mtime = str(pipfile_path.stat().st_mtime) |
| 732 | + |
| 733 | + # Include environment variables that affect resolution |
| 734 | + env_factors = [ |
| 735 | + os.environ.get("PIPENV_CACHE_VERSION", "1"), |
| 736 | + os.environ.get("PIPENV_PYPI_MIRROR", ""), |
| 737 | + os.environ.get("PIP_INDEX_URL", ""), |
| 738 | + str(pypi_mirror) if pypi_mirror else "", |
| 739 | + json.dumps(extra_pip_args, sort_keys=True) if extra_pip_args else "", |
| 740 | + ] |
| 741 | + |
| 742 | + # Create a deterministic representation of dependencies |
| 743 | + deps_str = json.dumps(deps, sort_keys=True) if isinstance(deps, dict) else str(deps) |
| 744 | + |
| 745 | + key_components = [ |
| 746 | + str(project.project_directory), |
| 747 | + lockfile_mtime, |
| 748 | + pipfile_mtime, |
| 749 | + deps_str, |
| 750 | + str(pipfile_category), |
| 751 | + str(pre), |
| 752 | + str(clear), |
| 753 | + str(allow_global), |
| 754 | + "|".join(env_factors), |
| 755 | + ] |
| 756 | + |
| 757 | + key_string = "|".join(key_components) |
| 758 | + return hashlib.md5(key_string.encode()).hexdigest() |
| 759 | + |
| 760 | + |
| 761 | +def _should_use_resolution_cache(cache_key, clear): |
| 762 | + """Check if we should use cached resolution results.""" |
| 763 | + if clear: |
| 764 | + return False |
| 765 | + |
| 766 | + if cache_key not in _resolution_cache: |
| 767 | + return False |
| 768 | + |
| 769 | + if cache_key not in _resolution_cache_timestamp: |
| 770 | + return False |
| 771 | + |
| 772 | + # Cache is valid for 10 minutes |
| 773 | + current_time = time.time() |
| 774 | + cache_age = current_time - _resolution_cache_timestamp[cache_key] |
| 775 | + return cache_age < 600 # 10 minutes |
| 776 | + |
| 777 | + |
710 | 778 | def _show_warning(message, category, filename, lineno, line):
|
711 | 779 | warnings.showwarning(
|
712 | 780 | message=message,
|
@@ -835,6 +903,31 @@ def venv_resolve_deps(
|
835 | 903 | lockfile = project.lockfile(categories=[pipfile_category])
|
836 | 904 | if old_lock_data is None:
|
837 | 905 | old_lock_data = lockfile.get(lockfile_category, {})
|
| 906 | + |
| 907 | + # Check cache before expensive resolution |
| 908 | + cache_key = _generate_resolution_cache_key( |
| 909 | + deps, |
| 910 | + project, |
| 911 | + pipfile_category, |
| 912 | + pre, |
| 913 | + clear, |
| 914 | + allow_global, |
| 915 | + pypi_mirror, |
| 916 | + extra_pip_args, |
| 917 | + ) |
| 918 | + |
| 919 | + if _should_use_resolution_cache(cache_key, clear): |
| 920 | + if project.s.is_verbose(): |
| 921 | + err.print("[dim]Using cached resolution results...[/dim]") |
| 922 | + cached_results = _resolution_cache[cache_key] |
| 923 | + return prepare_lockfile( |
| 924 | + project, |
| 925 | + cached_results, |
| 926 | + pipfile, |
| 927 | + lockfile.get(lockfile_category, {}), |
| 928 | + old_lock_data, |
| 929 | + ) |
| 930 | + |
838 | 931 | req_dir = create_tracked_tempdir(prefix="pipenv", suffix="requirements")
|
839 | 932 | results = []
|
840 | 933 | with temp_environ():
|
@@ -939,6 +1032,21 @@ def venv_resolve_deps(
|
939 | 1032 | )
|
940 | 1033 | err.print(f"Output: {c.stdout.strip()}")
|
941 | 1034 | err.print(f"Error: {c.stderr.strip()}")
|
| 1035 | + |
| 1036 | + # Cache the results for future use |
| 1037 | + if results: |
| 1038 | + _resolution_cache[cache_key] = results |
| 1039 | + _resolution_cache_timestamp[cache_key] = time.time() |
| 1040 | + |
| 1041 | + # Clean old cache entries (keep only last 5 projects) |
| 1042 | + if len(_resolution_cache) > 5: |
| 1043 | + oldest_key = min( |
| 1044 | + _resolution_cache_timestamp.keys(), |
| 1045 | + key=lambda k: _resolution_cache_timestamp[k], |
| 1046 | + ) |
| 1047 | + _resolution_cache.pop(oldest_key, None) |
| 1048 | + _resolution_cache_timestamp.pop(oldest_key, None) |
| 1049 | + |
942 | 1050 | if lockfile_category not in lockfile:
|
943 | 1051 | lockfile[lockfile_category] = {}
|
944 | 1052 | return prepare_lockfile(
|
|
0 commit comments