Skip to content

Commit f138910

Browse files
committed
Optimize resolver usage
1 parent c9029f6 commit f138910

File tree

7 files changed

+258
-64
lines changed

7 files changed

+258
-64
lines changed

benchmarks/benchmark.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import subprocess
88
import sys
99
import time
10+
import urllib.request
1011
from pathlib import Path
1112
from typing import List, Tuple
1213

@@ -68,8 +69,6 @@ def run_timed_command(
6869
def setup_requirements(self):
6970
"""Download and prepare requirements.txt."""
7071
print("Setting up requirements.txt...")
71-
import urllib.request
72-
7372
requirements_path = self.benchmark_dir / "requirements.txt"
7473

7574
try:

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 & 8 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,8 +17,10 @@
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
23+
from pipenv.patched.pip._vendor.packaging.version import Version
2124
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
2225
from pipenv.patched.pip._vendor.typing_extensions import Iterable
2326
from pipenv.utils import console
@@ -28,6 +31,8 @@
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):
@@ -102,8 +107,6 @@ def safe_import(self, name: str) -> ModuleType:
102107
def python_version(self) -> str | None:
103108
with self.activated() as active:
104109
if active:
105-
from pipenv.patched.pip._vendor.packaging.version import Version
106-
107110
# Extract version parts
108111
version_str = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
109112
python_version = Version(version_str) # Create PEP 440 compliant version
@@ -633,12 +636,6 @@ def _get_requirements_for_package(cls, node, key_tree, parent=None, chain=None):
633636
return d
634637

635638
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
641-
642639
flatten = chain.from_iterable
643640

644641
packages = self.get_installed_packages()

pipenv/project.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,100 @@ def add_pipfile_entry_to_pipfile(self, name, normalized_name, entry, category=No
12731273
self.write_toml(parsed_pipfile)
12741274
return newly_added, category, normalized_name
12751275

1276+
def add_packages_to_pipfile_batch(self, packages_data, dev=False, categories=None):
1277+
"""
1278+
Add multiple packages to Pipfile in a single operation for better performance.
1279+
1280+
Args:
1281+
packages_data: List of tuples (package, pip_line) or list of dicts with package info
1282+
dev: Whether to add to dev-packages section
1283+
categories: List of categories to add packages to
1284+
1285+
Returns:
1286+
List of tuples (newly_added, category, normalized_name) for each package
1287+
"""
1288+
if not packages_data:
1289+
return []
1290+
1291+
# Determine target categories
1292+
if categories is None:
1293+
categories = ["dev-packages" if dev else "packages"]
1294+
elif isinstance(categories, str):
1295+
categories = [categories]
1296+
1297+
# Read Pipfile once
1298+
parsed_pipfile = self.parsed_pipfile
1299+
results = []
1300+
1301+
# Ensure all categories exist
1302+
for category in categories:
1303+
if category not in parsed_pipfile:
1304+
parsed_pipfile[category] = {}
1305+
1306+
# Process all packages
1307+
for package_data in packages_data:
1308+
if isinstance(package_data, tuple) and len(package_data) == 2:
1309+
package, pip_line = package_data
1310+
1311+
# Generate entry for this package
1312+
name, normalized_name, entry = self.generate_package_pipfile_entry(
1313+
package, pip_line, category=categories[0]
1314+
)
1315+
1316+
# Add to each specified category
1317+
for category in categories:
1318+
newly_added = False
1319+
1320+
# Remove any existing entries with different casing
1321+
section = parsed_pipfile.get(category, {})
1322+
for entry_name in section.copy().keys():
1323+
if entry_name.lower() == normalized_name.lower():
1324+
del parsed_pipfile[category][entry_name]
1325+
1326+
# Check if this is a new package
1327+
if normalized_name not in parsed_pipfile[category]:
1328+
newly_added = True
1329+
1330+
# Add the package
1331+
parsed_pipfile[category][normalized_name] = entry
1332+
results.append((newly_added, category, normalized_name))
1333+
1334+
elif isinstance(package_data, dict):
1335+
# Handle pre-processed package data
1336+
name = package_data.get("name")
1337+
normalized_name = package_data.get("normalized_name")
1338+
entry = package_data.get("entry")
1339+
1340+
if name and normalized_name and entry:
1341+
for category in categories:
1342+
newly_added = False
1343+
1344+
# Remove any existing entries with different casing
1345+
section = parsed_pipfile.get(category, {})
1346+
for entry_name in section.copy().keys():
1347+
if entry_name.lower() == normalized_name.lower():
1348+
del parsed_pipfile[category][entry_name]
1349+
1350+
# Check if this is a new package
1351+
if normalized_name not in parsed_pipfile[category]:
1352+
newly_added = True
1353+
1354+
# Add the package
1355+
parsed_pipfile[category][normalized_name] = entry
1356+
results.append((newly_added, category, normalized_name))
1357+
1358+
# Sort categories if requested
1359+
if self.settings.get("sort_pipfile"):
1360+
for category in categories:
1361+
if category in parsed_pipfile:
1362+
parsed_pipfile[category] = self._sort_category(
1363+
parsed_pipfile[category]
1364+
)
1365+
1366+
# Write Pipfile once at the end
1367+
self.write_toml(parsed_pipfile)
1368+
return results
1369+
12761370
def src_name_from_url(self, index_url):
12771371
location = urllib.parse.urlsplit(index_url).netloc
12781372
if "." in location:

pipenv/utils/requirements.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ def import_requirements(project, r=None, dev=False, categories=None):
6868
contents = f.read()
6969
if categories is None:
7070
categories = []
71+
72+
# Collect indexes and trusted hosts first
7173
indexes = []
7274
trusted_hosts = []
73-
# Find and add extra indexes.
7475
for line in contents.split("\n"):
7576
index, extra_index, trusted_host, _ = parse_indexes(line.strip(), strict=True)
7677
if index:
@@ -79,8 +80,11 @@ def import_requirements(project, r=None, dev=False, categories=None):
7980
indexes.append(extra_index)
8081
if trusted_host:
8182
trusted_hosts.append(get_host_and_port(trusted_host))
82-
# Convert Path object to string to avoid 'PosixPath' has no attribute 'decode' error
83+
84+
# Collect all packages for batch processing
85+
packages_to_add = []
8386
req_path = str(r) if isinstance(r, Path) else r
87+
8488
for f in parse_requirements(req_path, session=PipSession()):
8589
package = install_req_from_parsed_requirement(f)
8690
if package.name not in BAD_PACKAGES:
@@ -91,29 +95,26 @@ def import_requirements(project, r=None, dev=False, categories=None):
9195
package_string = unquote(
9296
redact_auth_from_url(package.original_link.url)
9397
)
94-
95-
if categories:
96-
for category in categories:
97-
project.add_package_to_pipfile(
98-
package, package_string, dev=dev, category=category
99-
)
100-
else:
101-
project.add_package_to_pipfile(package, package_string, dev=dev)
10298
else:
10399
package_string = str(package.req)
104100
if package.markers:
105101
package_string += f" ; {package.markers}"
106-
if categories:
107-
for category in categories:
108-
project.add_package_to_pipfile(
109-
package, package_string, dev=dev, category=category
110-
)
111-
else:
112-
project.add_package_to_pipfile(package, package_string, dev=dev)
102+
103+
packages_to_add.append((package, package_string))
104+
105+
# Batch add all packages to Pipfile
106+
if packages_to_add:
107+
project.add_packages_to_pipfile_batch(
108+
packages_to_add, dev=dev, categories=categories
109+
)
110+
111+
# Add indexes after packages
113112
indexes = sorted(set(indexes))
114113
trusted_hosts = sorted(set(trusted_hosts))
115114
for index in indexes:
116115
add_index_to_pipfile(project, index, trusted_hosts)
116+
117+
# Recase pipfile once at the end
117118
project.recase_pipfile()
118119

119120

0 commit comments

Comments
 (0)