Skip to content

Commit 65d7b4a

Browse files
committed
Optimize resolver usage
1 parent c9029f6 commit 65d7b4a

File tree

8 files changed

+267
-68
lines changed

8 files changed

+267
-68
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ zipp = "==3.21.0"
3131

3232
[packages]
3333
pytz = "*"
34+
myproject = {ref = "main", git = "https://\\:****@github.com/user/myproject.git"}
3435

3536
[scripts]
3637
tests = "bash ./run-tests.sh"

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 or (isinstance(categories, list) and len(categories) == 0):
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: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pipenv.utils.pip import get_trusted_hosts
1616

1717

18-
def redact_netloc(netloc: str) -> Tuple[str]:
18+
def redact_netloc(netloc: str) -> str:
1919
"""
2020
Replace the sensitive data in a netloc with "****", if it exists, unless it's an environment variable.
2121
@@ -26,7 +26,7 @@ def redact_netloc(netloc: str) -> Tuple[str]:
2626
"""
2727
netloc, (user, password) = split_auth_from_netloc(netloc)
2828
if user is None:
29-
return (netloc,)
29+
return netloc
3030
if password is None:
3131
# Check if user is an environment variable
3232
if not re.match(r"\$\{\w+\}", user):
@@ -42,12 +42,16 @@ def redact_netloc(netloc: str) -> Tuple[str]:
4242
# If it is, leave it as is
4343
password = ":" + password
4444
user = quote(user)
45-
return (f"{user}{password}@{netloc}",)
45+
return f"{user}{password}@{netloc}"
4646

4747

4848
def redact_auth_from_url(url: str) -> str:
4949
"""Replace the password in a given url with ****."""
50-
return _transform_url(url, redact_netloc)[0]
50+
51+
def _redact_netloc_wrapper(netloc: str) -> Tuple[str]:
52+
return (redact_netloc(netloc),)
53+
54+
return _transform_url(url, _redact_netloc_wrapper)[0]
5155

5256

5357
def normalize_name(pkg) -> str:
@@ -68,9 +72,10 @@ def import_requirements(project, r=None, dev=False, categories=None):
6872
contents = f.read()
6973
if categories is None:
7074
categories = []
75+
76+
# Collect indexes and trusted hosts first
7177
indexes = []
7278
trusted_hosts = []
73-
# Find and add extra indexes.
7479
for line in contents.split("\n"):
7580
index, extra_index, trusted_host, _ = parse_indexes(line.strip(), strict=True)
7681
if index:
@@ -79,8 +84,11 @@ def import_requirements(project, r=None, dev=False, categories=None):
7984
indexes.append(extra_index)
8085
if trusted_host:
8186
trusted_hosts.append(get_host_and_port(trusted_host))
82-
# Convert Path object to string to avoid 'PosixPath' has no attribute 'decode' error
87+
88+
# Collect all packages for batch processing
89+
packages_to_add = []
8390
req_path = str(r) if isinstance(r, Path) else r
91+
8492
for f in parse_requirements(req_path, session=PipSession()):
8593
package = install_req_from_parsed_requirement(f)
8694
if package.name not in BAD_PACKAGES:
@@ -91,29 +99,26 @@ def import_requirements(project, r=None, dev=False, categories=None):
9199
package_string = unquote(
92100
redact_auth_from_url(package.original_link.url)
93101
)
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)
102102
else:
103103
package_string = str(package.req)
104104
if package.markers:
105105
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)
106+
107+
packages_to_add.append((package, package_string))
108+
109+
# Batch add all packages to Pipfile
110+
if packages_to_add:
111+
project.add_packages_to_pipfile_batch(
112+
packages_to_add, dev=dev, categories=categories
113+
)
114+
115+
# Add indexes after packages
113116
indexes = sorted(set(indexes))
114117
trusted_hosts = sorted(set(trusted_hosts))
115118
for index in indexes:
116119
add_index_to_pipfile(project, index, trusted_hosts)
120+
121+
# Recase pipfile once at the end
117122
project.recase_pipfile()
118123

119124

0 commit comments

Comments
 (0)