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