Skip to content

Commit 4f8a613

Browse files
committed
Import keyring lazily
1 parent 996d4fa commit 4f8a613

File tree

1 file changed

+47
-48
lines changed

1 file changed

+47
-48
lines changed

src/pip/_internal/network/auth.py

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
providing credentials in the context of network requests.
55
"""
66

7+
import functools
78
import os
89
import shutil
910
import subprocess
1011
import urllib.parse
1112
from abc import ABC, abstractmethod
12-
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Type
13+
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
1314

1415
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
1516
from pip._vendor.requests.models import Request, Response
@@ -37,59 +38,56 @@ class Credentials(NamedTuple):
3738
class KeyRingBaseProvider(ABC):
3839
"""Keyring base provider interface"""
3940

40-
@classmethod
4141
@abstractmethod
42-
def is_available(cls) -> bool:
42+
def is_available(self) -> bool:
4343
...
4444

45-
@classmethod
4645
@abstractmethod
47-
def get_auth_info(cls, url: str, username: Optional[str]) -> Optional[AuthInfo]:
46+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
4847
...
4948

50-
@classmethod
5149
@abstractmethod
52-
def save_auth_info(cls, url: str, username: str, password: str) -> None:
50+
def save_auth_info(self, url: str, username: str, password: str) -> None:
5351
...
5452

5553

5654
class KeyRingPythonProvider(KeyRingBaseProvider):
5755
"""Keyring interface which uses locally imported `keyring`"""
5856

59-
try:
60-
import keyring
61-
except ImportError:
62-
keyring = None # type: ignore[assignment]
57+
def __init__(self) -> None:
58+
try:
59+
import keyring
60+
except ImportError:
61+
keyring = None # type: ignore[assignment]
6362

64-
@classmethod
65-
def is_available(cls) -> bool:
66-
return cls.keyring is not None
63+
self.keyring = keyring
6764

68-
@classmethod
69-
def get_auth_info(cls, url: str, username: Optional[str]) -> Optional[AuthInfo]:
70-
if cls.is_available is False:
65+
def is_available(self) -> bool:
66+
return self.keyring is not None
67+
68+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
69+
if self.is_available is False:
7170
return None
7271

7372
# Support keyring's get_credential interface which supports getting
7473
# credentials without a username. This is only available for
7574
# keyring>=15.2.0.
76-
if hasattr(cls.keyring, "get_credential"):
75+
if hasattr(self.keyring, "get_credential"):
7776
logger.debug("Getting credentials from keyring for %s", url)
78-
cred = cls.keyring.get_credential(url, username)
77+
cred = self.keyring.get_credential(url, username)
7978
if cred is not None:
8079
return cred.username, cred.password
8180
return None
8281

8382
if username is not None:
8483
logger.debug("Getting password from keyring for %s", url)
85-
password = cls.keyring.get_password(url, username)
84+
password = self.keyring.get_password(url, username)
8685
if password:
8786
return username, password
8887
return None
8988

90-
@classmethod
91-
def save_auth_info(cls, url: str, username: str, password: str) -> None:
92-
cls.keyring.set_password(url, username, password)
89+
def save_auth_info(self, url: str, username: str, password: str) -> None:
90+
self.keyring.set_password(url, username, password)
9391

9492

9593
class KeyRingCliProvider(KeyRingBaseProvider):
@@ -101,38 +99,35 @@ class KeyRingCliProvider(KeyRingBaseProvider):
10199
PATH.
102100
"""
103101

104-
keyring = shutil.which("keyring")
102+
def __init__(self) -> None:
103+
self.keyring = shutil.which("keyring")
105104

106-
@classmethod
107-
def is_available(cls) -> bool:
108-
return cls.keyring is not None
105+
def is_available(self) -> bool:
106+
return self.keyring is not None
109107

110-
@classmethod
111-
def get_auth_info(cls, url: str, username: Optional[str]) -> Optional[AuthInfo]:
112-
if cls.is_available is False:
108+
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
109+
if self.is_available is False:
113110
return None
114111

115112
# This is the default implementation of keyring.get_credential
116113
# https://github.com/jaraco/keyring/blob/97689324abcf01bd1793d49063e7ca01e03d7d07/keyring/backend.py#L134-L139
117114
if username is not None:
118-
password = cls._get_password(url, username)
115+
password = self._get_password(url, username)
119116
if password is not None:
120117
return username, password
121118
return None
122119

123-
@classmethod
124-
def save_auth_info(cls, url: str, username: str, password: str) -> None:
125-
if not cls.is_available:
120+
def save_auth_info(self, url: str, username: str, password: str) -> None:
121+
if not self.is_available:
126122
raise RuntimeError("keyring is not available")
127-
return cls._set_password(url, username, password)
123+
return self._set_password(url, username, password)
128124

129-
@classmethod
130-
def _get_password(cls, service_name: str, username: str) -> Optional[str]:
125+
def _get_password(self, service_name: str, username: str) -> Optional[str]:
131126
"""Mirror the implemenation of keyring.get_password using cli"""
132-
if cls.keyring is None:
127+
if self.keyring is None:
133128
return None
134129

135-
cmd = [cls.keyring, "get", service_name, username]
130+
cmd = [self.keyring, "get", service_name, username]
136131
env = os.environ.copy()
137132
env["PYTHONIOENCODING"] = "utf-8"
138133
res = subprocess.run(
@@ -145,13 +140,12 @@ def _get_password(cls, service_name: str, username: str) -> Optional[str]:
145140
return None
146141
return res.stdout.decode("utf-8").strip("\n")
147142

148-
@classmethod
149-
def _set_password(cls, service_name: str, username: str, password: str) -> None:
143+
def _set_password(self, service_name: str, username: str, password: str) -> None:
150144
"""Mirror the implemenation of keyring.set_password using cli"""
151-
if cls.keyring is None:
145+
if self.keyring is None:
152146
return None
153147

154-
cmd = [cls.keyring, "set", service_name, username]
148+
cmd = [self.keyring, "set", service_name, username]
155149
input_ = password.encode("utf-8") + b"\n"
156150
env = os.environ.copy()
157151
env["PYTHONIOENCODING"] = "utf-8"
@@ -160,11 +154,16 @@ def _set_password(cls, service_name: str, username: str, password: str) -> None:
160154
return None
161155

162156

163-
def get_keyring_provider() -> Optional[Type[KeyRingBaseProvider]]:
164-
if KeyRingPythonProvider.is_available():
165-
return KeyRingPythonProvider
166-
if KeyRingCliProvider.is_available():
167-
return KeyRingCliProvider
157+
@functools.lru_cache(maxsize=1)
158+
def get_keyring_provider() -> Optional[KeyRingBaseProvider]:
159+
python_keyring = KeyRingPythonProvider()
160+
if python_keyring.is_available():
161+
return python_keyring
162+
163+
cli_keyring = KeyRingCliProvider()
164+
if cli_keyring.is_available():
165+
return cli_keyring
166+
168167
return None
169168

170169

0 commit comments

Comments
 (0)