diff --git a/news/2697.feature.md b/news/2697.feature.md new file mode 100644 index 0000000000..ffe5bbe422 --- /dev/null +++ b/news/2697.feature.md @@ -0,0 +1 @@ +Reuse the request sesison within the environment. diff --git a/src/pdm/cli/commands/publish/repository.py b/src/pdm/cli/commands/publish/repository.py index 5edd86a25e..6d05fca20d 100644 --- a/src/pdm/cli/commands/publish/repository.py +++ b/src/pdm/cli/commands/publish/repository.py @@ -2,7 +2,6 @@ import os import pathlib -import weakref from typing import TYPE_CHECKING, Any, Iterable from urllib.parse import urlparse, urlunparse @@ -29,7 +28,7 @@ def __init__( verify_ssl: bool | None = True, ) -> None: self.url = url - self.session = project.environment._build_session([]) + self.session = project.environment.session if verify_ssl is False: self.session.verify = verify_ssl elif ca_certs is not None: @@ -38,7 +37,6 @@ def __init__( self.ui = project.core.ui username, password = self._ensure_credentials(username, password) self.session.auth = (username, password) - weakref.finalize(self, self.session.close) def _ensure_credentials(self, username: str | None, password: str | None) -> tuple[str, str]: from pdm.models.auth import keyring diff --git a/src/pdm/environments/base.py b/src/pdm/environments/base.py index 76e8e6a9b7..42caeb341a 100644 --- a/src/pdm/environments/base.py +++ b/src/pdm/environments/base.py @@ -12,7 +12,8 @@ from contextlib import contextmanager from functools import cached_property, partial from pathlib import Path -from typing import TYPE_CHECKING, Generator, no_type_check +from threading import local +from typing import TYPE_CHECKING, Generator, cast, no_type_check from pdm.exceptions import BuildError, PdmUsageError from pdm.models.in_process import get_pep508_environment, get_python_abis, get_uname, sysconfig_get_platform @@ -71,6 +72,8 @@ def __init__(self, project: Project, *, python: str | None = None) -> None: else: self._interpreter = PythonInfo.from_path(python) + self._local_cache = local() + @property def is_global(self) -> bool: """For backward compatibility, it is opposite to ``is_local``.""" @@ -109,10 +112,11 @@ def target_python(self) -> unearth.TargetPython: tp.supported_tags() return tp - def _build_session(self, trusted_hosts: list[str]) -> PDMSession: + def _build_session(self) -> PDMSession: from pdm.models.session import PDMSession ca_certs = self.project.config.get("pypi.ca_certs") + trusted_hosts = get_trusted_hosts(self.project.sources) session = PDMSession( cache_dir=self.project.cache("http"), trusted_hosts=trusted_hosts, @@ -125,8 +129,18 @@ def _build_session(self, trusted_hosts: list[str]) -> PDMSession: session.cert = (Path(certfn), Path(keyfn) if keyfn else None) session.auth = self.auth + self.project.core.exit_stack.callback(session.close) return session + @property + def session(self) -> PDMSession: + """Build the session and cache it in the thread local storage.""" + sess = getattr(self._local_cache, "session", None) + if sess is None: + sess = self._build_session() + self._local_cache.session = sess + return cast("PDMSession", sess) + @contextmanager def _patch_target_python(self) -> Generator[None, None, None]: """Patch the packaging modules to respect the arch of target python.""" @@ -178,11 +192,8 @@ def get_finder( f"{self.project.config['pypi.ignore_stored_index']}" ) - trusted_hosts = get_trusted_hosts(sources) - - session = self._build_session(trusted_hosts) finder = PDMPackageFinder( - session=session, + session=self.session, target_python=self.target_python, ignore_compatibility=ignore_compatibility, no_binary=os.getenv("PDM_NO_BINARY", "").split(","), @@ -201,10 +212,7 @@ def get_finder( finder.add_find_links(source.url) else: finder.add_index_url(source.url) - try: - yield finder - finally: - session.close() + yield finder def get_working_set(self) -> WorkingSet: """Get the working set based on local packages directory.""" diff --git a/tests/models/test_candidates.py b/tests/models/test_candidates.py index 229bb6672c..9dcdee6196 100644 --- a/tests/models/test_candidates.py +++ b/tests/models/test_candidates.py @@ -3,7 +3,6 @@ import pytest from unearth import Link -from pdm._types import RepositoryConfig from pdm.models.candidates import Candidate from pdm.models.requirements import parse_requirement from pdm.utils import is_path_relative_to, path_to_url @@ -277,16 +276,11 @@ def test_ignore_invalid_py_version(project): def test_find_candidates_from_find_links(project): - repo = project.get_repository() - repo.sources = [ - RepositoryConfig( - name="test", - config_prefix="pypi", - url="http://fixtures.test/index/demo.html", - verify_ssl=False, - type="find_links", - ) + project.pyproject.settings["source"] = [ + {"name": "test", "url": "http://fixtures.test/index/demo.html", "verify_ssl": False, "type": "find_links"} ] + project.pyproject.write(False) + repo = project.get_repository() candidates = list(repo.find_candidates(parse_requirement("demo"))) assert len(candidates) == 2 diff --git a/tests/test_project.py b/tests/test_project.py index d9ea9290c0..7b568885cc 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -397,8 +397,8 @@ def test_preserve_log_file(project, pdm, tmp_path, mocker): all_logs = list(tmp_path.joinpath("logs").iterdir()) assert len(all_logs) == 0 - with mocker.patch.object(project.core.synchronizer_class, "synchronize", side_effect=Exception): - result = pdm(["add", "pytz"], obj=project) - assert result.exit_code != 0 - install_log = next(tmp_path.joinpath("logs").glob("pdm-install-*.log")) - assert install_log.exists() + mocker.patch.object(project.core.synchronizer_class, "synchronize", side_effect=Exception) + result = pdm(["add", "pytz"], obj=project) + assert result.exit_code != 0 + install_log = next(tmp_path.joinpath("logs").glob("pdm-install-*.log")) + assert install_log.exists()