Skip to content

Commit 19f988a

Browse files
committed
fix lint
1 parent ea0f52b commit 19f988a

File tree

3 files changed

+115
-11
lines changed

3 files changed

+115
-11
lines changed

pipenv/cli/options.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from pipenv.project import Project
55
from pipenv.utils import console, err
6+
from pipenv.utils.display import format_help
67
from pipenv.utils.internet import is_valid_url
78
from pipenv.vendor.click import (
89
BadArgumentUsage,
@@ -23,8 +24,6 @@ class PipenvGroup(DYMMixin, Group):
2324
"""Custom Group class provides formatted main help"""
2425

2526
def get_help_option(self, ctx):
26-
from pipenv.utils.display import format_help
27-
2827
"""Override for showing formatted main help via --help and -h options"""
2928
help_options = self.get_help_option_names(ctx)
3029
if not help_options or not self.add_help_option:

pipenv/environment.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99
import typing
1010
from functools import cached_property
11+
from itertools import chain
1112
from pathlib import Path
1213
from sysconfig import get_paths, get_python_version, get_scheme_names
1314
from urllib.parse import urlparse
@@ -16,6 +17,7 @@
1617
from pipenv.patched.pip._internal.commands.install import InstallCommand
1718
from pipenv.patched.pip._internal.index.package_finder import PackageFinder
1819
from pipenv.patched.pip._internal.req.req_install import InstallRequirement
20+
from pipenv.patched.pip._vendor.packaging.markers import UndefinedEnvironmentName
1921
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
2022
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
2123
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
@@ -25,9 +27,12 @@
2527
from pipenv.utils.funktools import chunked, unnest
2628
from pipenv.utils.indexes import prepare_pip_source_args
2729
from pipenv.utils.processes import subprocess_run
30+
from pipenv.utils.resolver import get_package_finder
2831
from pipenv.utils.shell import temp_environ
2932
from pipenv.utils.virtualenv import virtualenv_scripts_dir
3033
from pipenv.vendor.importlib_metadata.compat.py39 import normalized_name
34+
from pipenv.vendor.pipdeptree._models.dag import PackageDAG
35+
from pipenv.vendor.pipdeptree._models.package import InvalidRequirementError
3136
from pipenv.vendor.pythonfinder.utils import is_in_path
3237

3338
if sys.version_info < (3, 10):
@@ -262,8 +267,6 @@ def sys_path(self) -> list[str]:
262267
:return: The :data:`sys.path` from the environment
263268
:rtype: list
264269
"""
265-
import json
266-
267270
current_executable = Path(sys.executable).as_posix()
268271
if not self.python or self.python == current_executable:
269272
return sys.path
@@ -551,8 +554,6 @@ def get_installed_packages(self) -> list[importlib_metadata.Distribution]:
551554

552555
@contextlib.contextmanager
553556
def get_finder(self, pre: bool = False) -> ContextManager[PackageFinder]:
554-
from .utils.resolver import get_package_finder
555-
556557
pip_command = InstallCommand(
557558
name="InstallCommand", summary="pip Install command."
558559
)
@@ -633,11 +634,6 @@ def _get_requirements_for_package(cls, node, key_tree, parent=None, chain=None):
633634
return d
634635

635636
def get_package_requirements(self, pkg=None):
636-
from itertools import chain
637-
638-
from pipenv.patched.pip._vendor.packaging.markers import UndefinedEnvironmentName
639-
from pipenv.vendor.pipdeptree._models.dag import PackageDAG
640-
from pipenv.vendor.pipdeptree._models.package import InvalidRequirementError
641637

642638
flatten = chain.from_iterable
643639

pipenv/utils/resolver.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import contextlib
2+
import hashlib
23
import json
34
import os
45
import subprocess
56
import sys
67
import tempfile
8+
import time
79
import warnings
810
from pathlib import Path
911
from typing import Any, Dict, List, Optional, Tuple, Union
@@ -707,6 +709,73 @@ def clean_results(self) -> List[Dict[str, Any]]:
707709
return list(results.values())
708710

709711

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+
710779
def _show_warning(message, category, filename, lineno, line):
711780
warnings.showwarning(
712781
message=message,
@@ -835,6 +904,31 @@ def venv_resolve_deps(
835904
lockfile = project.lockfile(categories=[pipfile_category])
836905
if old_lock_data is None:
837906
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+
838932
req_dir = create_tracked_tempdir(prefix="pipenv", suffix="requirements")
839933
results = []
840934
with temp_environ():
@@ -939,6 +1033,21 @@ def venv_resolve_deps(
9391033
)
9401034
err.print(f"Output: {c.stdout.strip()}")
9411035
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+
9421051
if lockfile_category not in lockfile:
9431052
lockfile[lockfile_category] = {}
9441053
return prepare_lockfile(

0 commit comments

Comments
 (0)