pip 25.1__py3-none-any.whl → 25.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. pip/__init__.py +3 -3
  2. pip/_internal/__init__.py +2 -2
  3. pip/_internal/build_env.py +118 -94
  4. pip/_internal/cache.py +16 -14
  5. pip/_internal/cli/autocompletion.py +13 -4
  6. pip/_internal/cli/base_command.py +18 -7
  7. pip/_internal/cli/cmdoptions.py +14 -9
  8. pip/_internal/cli/command_context.py +4 -3
  9. pip/_internal/cli/index_command.py +11 -9
  10. pip/_internal/cli/main.py +3 -2
  11. pip/_internal/cli/main_parser.py +4 -3
  12. pip/_internal/cli/parser.py +26 -22
  13. pip/_internal/cli/progress_bars.py +19 -12
  14. pip/_internal/cli/req_command.py +16 -12
  15. pip/_internal/cli/spinners.py +81 -5
  16. pip/_internal/commands/__init__.py +5 -3
  17. pip/_internal/commands/cache.py +18 -15
  18. pip/_internal/commands/check.py +1 -2
  19. pip/_internal/commands/completion.py +1 -2
  20. pip/_internal/commands/configuration.py +26 -18
  21. pip/_internal/commands/debug.py +8 -6
  22. pip/_internal/commands/download.py +2 -3
  23. pip/_internal/commands/freeze.py +2 -3
  24. pip/_internal/commands/hash.py +1 -2
  25. pip/_internal/commands/help.py +1 -2
  26. pip/_internal/commands/index.py +15 -9
  27. pip/_internal/commands/inspect.py +4 -4
  28. pip/_internal/commands/install.py +45 -40
  29. pip/_internal/commands/list.py +35 -26
  30. pip/_internal/commands/lock.py +1 -2
  31. pip/_internal/commands/search.py +14 -12
  32. pip/_internal/commands/show.py +14 -11
  33. pip/_internal/commands/uninstall.py +1 -2
  34. pip/_internal/commands/wheel.py +2 -3
  35. pip/_internal/configuration.py +39 -25
  36. pip/_internal/distributions/base.py +6 -4
  37. pip/_internal/distributions/installed.py +8 -4
  38. pip/_internal/distributions/sdist.py +20 -13
  39. pip/_internal/distributions/wheel.py +6 -4
  40. pip/_internal/exceptions.py +58 -39
  41. pip/_internal/index/collector.py +24 -29
  42. pip/_internal/index/package_finder.py +70 -61
  43. pip/_internal/index/sources.py +17 -14
  44. pip/_internal/locations/__init__.py +18 -16
  45. pip/_internal/locations/_distutils.py +12 -11
  46. pip/_internal/locations/_sysconfig.py +5 -4
  47. pip/_internal/locations/base.py +4 -3
  48. pip/_internal/main.py +2 -2
  49. pip/_internal/metadata/__init__.py +8 -6
  50. pip/_internal/metadata/_json.py +5 -4
  51. pip/_internal/metadata/base.py +22 -27
  52. pip/_internal/metadata/importlib/_compat.py +6 -4
  53. pip/_internal/metadata/importlib/_dists.py +12 -17
  54. pip/_internal/metadata/importlib/_envs.py +9 -6
  55. pip/_internal/metadata/pkg_resources.py +11 -14
  56. pip/_internal/models/direct_url.py +24 -21
  57. pip/_internal/models/format_control.py +5 -5
  58. pip/_internal/models/installation_report.py +4 -3
  59. pip/_internal/models/link.py +39 -34
  60. pip/_internal/models/pylock.py +27 -22
  61. pip/_internal/models/search_scope.py +6 -7
  62. pip/_internal/models/selection_prefs.py +3 -3
  63. pip/_internal/models/target_python.py +10 -9
  64. pip/_internal/models/wheel.py +7 -5
  65. pip/_internal/network/auth.py +20 -22
  66. pip/_internal/network/cache.py +22 -6
  67. pip/_internal/network/download.py +169 -141
  68. pip/_internal/network/lazy_wheel.py +10 -7
  69. pip/_internal/network/session.py +32 -27
  70. pip/_internal/network/utils.py +2 -2
  71. pip/_internal/network/xmlrpc.py +2 -2
  72. pip/_internal/operations/build/build_tracker.py +10 -8
  73. pip/_internal/operations/build/wheel.py +3 -2
  74. pip/_internal/operations/build/wheel_editable.py +3 -2
  75. pip/_internal/operations/build/wheel_legacy.py +9 -8
  76. pip/_internal/operations/check.py +21 -26
  77. pip/_internal/operations/freeze.py +12 -9
  78. pip/_internal/operations/install/editable_legacy.py +5 -3
  79. pip/_internal/operations/install/wheel.py +53 -44
  80. pip/_internal/operations/prepare.py +35 -30
  81. pip/_internal/pyproject.py +7 -10
  82. pip/_internal/req/__init__.py +12 -10
  83. pip/_internal/req/constructors.py +33 -31
  84. pip/_internal/req/req_dependency_group.py +9 -8
  85. pip/_internal/req/req_file.py +32 -35
  86. pip/_internal/req/req_install.py +37 -34
  87. pip/_internal/req/req_set.py +4 -5
  88. pip/_internal/req/req_uninstall.py +20 -17
  89. pip/_internal/resolution/base.py +3 -3
  90. pip/_internal/resolution/legacy/resolver.py +21 -20
  91. pip/_internal/resolution/resolvelib/base.py +16 -13
  92. pip/_internal/resolution/resolvelib/candidates.py +29 -26
  93. pip/_internal/resolution/resolvelib/factory.py +41 -50
  94. pip/_internal/resolution/resolvelib/found_candidates.py +11 -9
  95. pip/_internal/resolution/resolvelib/provider.py +15 -20
  96. pip/_internal/resolution/resolvelib/reporter.py +5 -3
  97. pip/_internal/resolution/resolvelib/requirements.py +8 -6
  98. pip/_internal/resolution/resolvelib/resolver.py +39 -23
  99. pip/_internal/self_outdated_check.py +8 -6
  100. pip/_internal/utils/appdirs.py +1 -2
  101. pip/_internal/utils/compat.py +7 -1
  102. pip/_internal/utils/compatibility_tags.py +17 -16
  103. pip/_internal/utils/deprecation.py +11 -9
  104. pip/_internal/utils/direct_url_helpers.py +2 -2
  105. pip/_internal/utils/egg_link.py +6 -5
  106. pip/_internal/utils/entrypoints.py +3 -2
  107. pip/_internal/utils/filesystem.py +8 -5
  108. pip/_internal/utils/filetypes.py +4 -6
  109. pip/_internal/utils/glibc.py +6 -5
  110. pip/_internal/utils/hashes.py +9 -6
  111. pip/_internal/utils/logging.py +8 -5
  112. pip/_internal/utils/misc.py +54 -44
  113. pip/_internal/utils/packaging.py +3 -2
  114. pip/_internal/utils/retry.py +7 -4
  115. pip/_internal/utils/setuptools_build.py +12 -10
  116. pip/_internal/utils/subprocess.py +20 -17
  117. pip/_internal/utils/temp_dir.py +10 -12
  118. pip/_internal/utils/unpacking.py +6 -4
  119. pip/_internal/utils/urls.py +1 -1
  120. pip/_internal/utils/virtualenv.py +3 -2
  121. pip/_internal/utils/wheel.py +3 -4
  122. pip/_internal/vcs/bazaar.py +26 -8
  123. pip/_internal/vcs/git.py +59 -24
  124. pip/_internal/vcs/mercurial.py +34 -11
  125. pip/_internal/vcs/subversion.py +27 -16
  126. pip/_internal/vcs/versioncontrol.py +56 -51
  127. pip/_internal/wheel_builder.py +14 -12
  128. pip/_vendor/cachecontrol/__init__.py +1 -1
  129. pip/_vendor/certifi/__init__.py +1 -1
  130. pip/_vendor/certifi/cacert.pem +102 -221
  131. pip/_vendor/certifi/core.py +1 -32
  132. pip/_vendor/dependency_groups/_implementation.py +7 -11
  133. pip/_vendor/distlib/__init__.py +2 -2
  134. pip/_vendor/distlib/scripts.py +1 -1
  135. pip/_vendor/msgpack/__init__.py +2 -2
  136. pip/_vendor/pkg_resources/__init__.py +1 -1
  137. pip/_vendor/platformdirs/version.py +2 -2
  138. pip/_vendor/pygments/__init__.py +1 -1
  139. pip/_vendor/requests/__version__.py +2 -2
  140. pip/_vendor/requests/compat.py +12 -0
  141. pip/_vendor/requests/models.py +3 -1
  142. pip/_vendor/requests/utils.py +6 -16
  143. pip/_vendor/resolvelib/__init__.py +3 -3
  144. pip/_vendor/resolvelib/reporters.py +1 -1
  145. pip/_vendor/resolvelib/resolvers/__init__.py +4 -4
  146. pip/_vendor/resolvelib/resolvers/resolution.py +91 -10
  147. pip/_vendor/rich/__main__.py +12 -40
  148. pip/_vendor/rich/_inspect.py +1 -1
  149. pip/_vendor/rich/_ratio.py +1 -7
  150. pip/_vendor/rich/align.py +1 -7
  151. pip/_vendor/rich/box.py +1 -7
  152. pip/_vendor/rich/console.py +25 -20
  153. pip/_vendor/rich/control.py +1 -7
  154. pip/_vendor/rich/diagnose.py +1 -0
  155. pip/_vendor/rich/emoji.py +1 -6
  156. pip/_vendor/rich/live.py +32 -7
  157. pip/_vendor/rich/live_render.py +1 -7
  158. pip/_vendor/rich/logging.py +1 -1
  159. pip/_vendor/rich/panel.py +3 -4
  160. pip/_vendor/rich/progress.py +15 -15
  161. pip/_vendor/rich/spinner.py +7 -13
  162. pip/_vendor/rich/syntax.py +24 -5
  163. pip/_vendor/rich/traceback.py +32 -17
  164. pip/_vendor/truststore/_api.py +1 -1
  165. pip/_vendor/vendor.txt +10 -11
  166. {pip-25.1.dist-info → pip-25.2.dist-info}/METADATA +26 -4
  167. {pip-25.1.dist-info → pip-25.2.dist-info}/RECORD +194 -181
  168. {pip-25.1.dist-info → pip-25.2.dist-info}/WHEEL +1 -1
  169. {pip-25.1.dist-info → pip-25.2.dist-info}/licenses/AUTHORS.txt +12 -0
  170. pip-25.2.dist-info/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt +13 -0
  171. pip-25.2.dist-info/licenses/src/pip/_vendor/certifi/LICENSE +20 -0
  172. pip-25.2.dist-info/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt +9 -0
  173. pip-25.2.dist-info/licenses/src/pip/_vendor/distlib/LICENSE.txt +284 -0
  174. pip-25.2.dist-info/licenses/src/pip/_vendor/distro/LICENSE +202 -0
  175. pip-25.2.dist-info/licenses/src/pip/_vendor/idna/LICENSE.md +31 -0
  176. pip-25.2.dist-info/licenses/src/pip/_vendor/msgpack/COPYING +14 -0
  177. pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE +3 -0
  178. pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.APACHE +177 -0
  179. pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.BSD +23 -0
  180. pip-25.2.dist-info/licenses/src/pip/_vendor/pkg_resources/LICENSE +17 -0
  181. pip-25.2.dist-info/licenses/src/pip/_vendor/platformdirs/LICENSE +21 -0
  182. pip-25.2.dist-info/licenses/src/pip/_vendor/pygments/LICENSE +25 -0
  183. pip-25.2.dist-info/licenses/src/pip/_vendor/pyproject_hooks/LICENSE +21 -0
  184. pip-25.2.dist-info/licenses/src/pip/_vendor/requests/LICENSE +175 -0
  185. pip-25.2.dist-info/licenses/src/pip/_vendor/resolvelib/LICENSE +13 -0
  186. pip-25.2.dist-info/licenses/src/pip/_vendor/rich/LICENSE +19 -0
  187. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE +21 -0
  188. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE-HEADER +3 -0
  189. pip-25.2.dist-info/licenses/src/pip/_vendor/tomli_w/LICENSE +21 -0
  190. pip-25.2.dist-info/licenses/src/pip/_vendor/truststore/LICENSE +21 -0
  191. pip-25.2.dist-info/licenses/src/pip/_vendor/urllib3/LICENSE.txt +21 -0
  192. pip/_vendor/distlib/database.py +0 -1329
  193. pip/_vendor/distlib/index.py +0 -508
  194. pip/_vendor/distlib/locators.py +0 -1295
  195. pip/_vendor/distlib/manifest.py +0 -384
  196. pip/_vendor/distlib/markers.py +0 -162
  197. pip/_vendor/distlib/metadata.py +0 -1031
  198. pip/_vendor/distlib/version.py +0 -750
  199. pip/_vendor/distlib/wheel.py +0 -1100
  200. pip/_vendor/typing_extensions.py +0 -4584
  201. {pip-25.1.dist-info → pip-25.2.dist-info}/entry_points.txt +0 -0
  202. {pip-25.1.dist-info → pip-25.2.dist-info}/licenses/LICENSE.txt +0 -0
  203. {pip-25.1.dist-info → pip-25.2.dist-info}/top_level.txt +0 -0
@@ -1,508 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- #
3
- # Copyright (C) 2013-2023 Vinay Sajip.
4
- # Licensed to the Python Software Foundation under a contributor agreement.
5
- # See LICENSE.txt and CONTRIBUTORS.txt.
6
- #
7
- import hashlib
8
- import logging
9
- import os
10
- import shutil
11
- import subprocess
12
- import tempfile
13
- try:
14
- from threading import Thread
15
- except ImportError: # pragma: no cover
16
- from dummy_threading import Thread
17
-
18
- from . import DistlibException
19
- from .compat import (HTTPBasicAuthHandler, Request, HTTPPasswordMgr,
20
- urlparse, build_opener, string_types)
21
- from .util import zip_dir, ServerProxy
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
- DEFAULT_INDEX = 'https://pypi.org/pypi'
26
- DEFAULT_REALM = 'pypi'
27
-
28
-
29
- class PackageIndex(object):
30
- """
31
- This class represents a package index compatible with PyPI, the Python
32
- Package Index.
33
- """
34
-
35
- boundary = b'----------ThIs_Is_tHe_distlib_index_bouNdaRY_$'
36
-
37
- def __init__(self, url=None):
38
- """
39
- Initialise an instance.
40
-
41
- :param url: The URL of the index. If not specified, the URL for PyPI is
42
- used.
43
- """
44
- self.url = url or DEFAULT_INDEX
45
- self.read_configuration()
46
- scheme, netloc, path, params, query, frag = urlparse(self.url)
47
- if params or query or frag or scheme not in ('http', 'https'):
48
- raise DistlibException('invalid repository: %s' % self.url)
49
- self.password_handler = None
50
- self.ssl_verifier = None
51
- self.gpg = None
52
- self.gpg_home = None
53
- with open(os.devnull, 'w') as sink:
54
- # Use gpg by default rather than gpg2, as gpg2 insists on
55
- # prompting for passwords
56
- for s in ('gpg', 'gpg2'):
57
- try:
58
- rc = subprocess.check_call([s, '--version'], stdout=sink,
59
- stderr=sink)
60
- if rc == 0:
61
- self.gpg = s
62
- break
63
- except OSError:
64
- pass
65
-
66
- def _get_pypirc_command(self):
67
- """
68
- Get the distutils command for interacting with PyPI configurations.
69
- :return: the command.
70
- """
71
- from .util import _get_pypirc_command as cmd
72
- return cmd()
73
-
74
- def read_configuration(self):
75
- """
76
- Read the PyPI access configuration as supported by distutils. This populates
77
- ``username``, ``password``, ``realm`` and ``url`` attributes from the
78
- configuration.
79
- """
80
- from .util import _load_pypirc
81
- cfg = _load_pypirc(self)
82
- self.username = cfg.get('username')
83
- self.password = cfg.get('password')
84
- self.realm = cfg.get('realm', 'pypi')
85
- self.url = cfg.get('repository', self.url)
86
-
87
- def save_configuration(self):
88
- """
89
- Save the PyPI access configuration. You must have set ``username`` and
90
- ``password`` attributes before calling this method.
91
- """
92
- self.check_credentials()
93
- from .util import _store_pypirc
94
- _store_pypirc(self)
95
-
96
- def check_credentials(self):
97
- """
98
- Check that ``username`` and ``password`` have been set, and raise an
99
- exception if not.
100
- """
101
- if self.username is None or self.password is None:
102
- raise DistlibException('username and password must be set')
103
- pm = HTTPPasswordMgr()
104
- _, netloc, _, _, _, _ = urlparse(self.url)
105
- pm.add_password(self.realm, netloc, self.username, self.password)
106
- self.password_handler = HTTPBasicAuthHandler(pm)
107
-
108
- def register(self, metadata): # pragma: no cover
109
- """
110
- Register a distribution on PyPI, using the provided metadata.
111
-
112
- :param metadata: A :class:`Metadata` instance defining at least a name
113
- and version number for the distribution to be
114
- registered.
115
- :return: The HTTP response received from PyPI upon submission of the
116
- request.
117
- """
118
- self.check_credentials()
119
- metadata.validate()
120
- d = metadata.todict()
121
- d[':action'] = 'verify'
122
- request = self.encode_request(d.items(), [])
123
- self.send_request(request)
124
- d[':action'] = 'submit'
125
- request = self.encode_request(d.items(), [])
126
- return self.send_request(request)
127
-
128
- def _reader(self, name, stream, outbuf):
129
- """
130
- Thread runner for reading lines of from a subprocess into a buffer.
131
-
132
- :param name: The logical name of the stream (used for logging only).
133
- :param stream: The stream to read from. This will typically a pipe
134
- connected to the output stream of a subprocess.
135
- :param outbuf: The list to append the read lines to.
136
- """
137
- while True:
138
- s = stream.readline()
139
- if not s:
140
- break
141
- s = s.decode('utf-8').rstrip()
142
- outbuf.append(s)
143
- logger.debug('%s: %s' % (name, s))
144
- stream.close()
145
-
146
- def get_sign_command(self, filename, signer, sign_password, keystore=None): # pragma: no cover
147
- """
148
- Return a suitable command for signing a file.
149
-
150
- :param filename: The pathname to the file to be signed.
151
- :param signer: The identifier of the signer of the file.
152
- :param sign_password: The passphrase for the signer's
153
- private key used for signing.
154
- :param keystore: The path to a directory which contains the keys
155
- used in verification. If not specified, the
156
- instance's ``gpg_home`` attribute is used instead.
157
- :return: The signing command as a list suitable to be
158
- passed to :class:`subprocess.Popen`.
159
- """
160
- cmd = [self.gpg, '--status-fd', '2', '--no-tty']
161
- if keystore is None:
162
- keystore = self.gpg_home
163
- if keystore:
164
- cmd.extend(['--homedir', keystore])
165
- if sign_password is not None:
166
- cmd.extend(['--batch', '--passphrase-fd', '0'])
167
- td = tempfile.mkdtemp()
168
- sf = os.path.join(td, os.path.basename(filename) + '.asc')
169
- cmd.extend(['--detach-sign', '--armor', '--local-user',
170
- signer, '--output', sf, filename])
171
- logger.debug('invoking: %s', ' '.join(cmd))
172
- return cmd, sf
173
-
174
- def run_command(self, cmd, input_data=None):
175
- """
176
- Run a command in a child process , passing it any input data specified.
177
-
178
- :param cmd: The command to run.
179
- :param input_data: If specified, this must be a byte string containing
180
- data to be sent to the child process.
181
- :return: A tuple consisting of the subprocess' exit code, a list of
182
- lines read from the subprocess' ``stdout``, and a list of
183
- lines read from the subprocess' ``stderr``.
184
- """
185
- kwargs = {
186
- 'stdout': subprocess.PIPE,
187
- 'stderr': subprocess.PIPE,
188
- }
189
- if input_data is not None:
190
- kwargs['stdin'] = subprocess.PIPE
191
- stdout = []
192
- stderr = []
193
- p = subprocess.Popen(cmd, **kwargs)
194
- # We don't use communicate() here because we may need to
195
- # get clever with interacting with the command
196
- t1 = Thread(target=self._reader, args=('stdout', p.stdout, stdout))
197
- t1.start()
198
- t2 = Thread(target=self._reader, args=('stderr', p.stderr, stderr))
199
- t2.start()
200
- if input_data is not None:
201
- p.stdin.write(input_data)
202
- p.stdin.close()
203
-
204
- p.wait()
205
- t1.join()
206
- t2.join()
207
- return p.returncode, stdout, stderr
208
-
209
- def sign_file(self, filename, signer, sign_password, keystore=None): # pragma: no cover
210
- """
211
- Sign a file.
212
-
213
- :param filename: The pathname to the file to be signed.
214
- :param signer: The identifier of the signer of the file.
215
- :param sign_password: The passphrase for the signer's
216
- private key used for signing.
217
- :param keystore: The path to a directory which contains the keys
218
- used in signing. If not specified, the instance's
219
- ``gpg_home`` attribute is used instead.
220
- :return: The absolute pathname of the file where the signature is
221
- stored.
222
- """
223
- cmd, sig_file = self.get_sign_command(filename, signer, sign_password,
224
- keystore)
225
- rc, stdout, stderr = self.run_command(cmd,
226
- sign_password.encode('utf-8'))
227
- if rc != 0:
228
- raise DistlibException('sign command failed with error '
229
- 'code %s' % rc)
230
- return sig_file
231
-
232
- def upload_file(self, metadata, filename, signer=None, sign_password=None,
233
- filetype='sdist', pyversion='source', keystore=None):
234
- """
235
- Upload a release file to the index.
236
-
237
- :param metadata: A :class:`Metadata` instance defining at least a name
238
- and version number for the file to be uploaded.
239
- :param filename: The pathname of the file to be uploaded.
240
- :param signer: The identifier of the signer of the file.
241
- :param sign_password: The passphrase for the signer's
242
- private key used for signing.
243
- :param filetype: The type of the file being uploaded. This is the
244
- distutils command which produced that file, e.g.
245
- ``sdist`` or ``bdist_wheel``.
246
- :param pyversion: The version of Python which the release relates
247
- to. For code compatible with any Python, this would
248
- be ``source``, otherwise it would be e.g. ``3.2``.
249
- :param keystore: The path to a directory which contains the keys
250
- used in signing. If not specified, the instance's
251
- ``gpg_home`` attribute is used instead.
252
- :return: The HTTP response received from PyPI upon submission of the
253
- request.
254
- """
255
- self.check_credentials()
256
- if not os.path.exists(filename):
257
- raise DistlibException('not found: %s' % filename)
258
- metadata.validate()
259
- d = metadata.todict()
260
- sig_file = None
261
- if signer:
262
- if not self.gpg:
263
- logger.warning('no signing program available - not signed')
264
- else:
265
- sig_file = self.sign_file(filename, signer, sign_password,
266
- keystore)
267
- with open(filename, 'rb') as f:
268
- file_data = f.read()
269
- md5_digest = hashlib.md5(file_data).hexdigest()
270
- sha256_digest = hashlib.sha256(file_data).hexdigest()
271
- d.update({
272
- ':action': 'file_upload',
273
- 'protocol_version': '1',
274
- 'filetype': filetype,
275
- 'pyversion': pyversion,
276
- 'md5_digest': md5_digest,
277
- 'sha256_digest': sha256_digest,
278
- })
279
- files = [('content', os.path.basename(filename), file_data)]
280
- if sig_file:
281
- with open(sig_file, 'rb') as f:
282
- sig_data = f.read()
283
- files.append(('gpg_signature', os.path.basename(sig_file),
284
- sig_data))
285
- shutil.rmtree(os.path.dirname(sig_file))
286
- request = self.encode_request(d.items(), files)
287
- return self.send_request(request)
288
-
289
- def upload_documentation(self, metadata, doc_dir): # pragma: no cover
290
- """
291
- Upload documentation to the index.
292
-
293
- :param metadata: A :class:`Metadata` instance defining at least a name
294
- and version number for the documentation to be
295
- uploaded.
296
- :param doc_dir: The pathname of the directory which contains the
297
- documentation. This should be the directory that
298
- contains the ``index.html`` for the documentation.
299
- :return: The HTTP response received from PyPI upon submission of the
300
- request.
301
- """
302
- self.check_credentials()
303
- if not os.path.isdir(doc_dir):
304
- raise DistlibException('not a directory: %r' % doc_dir)
305
- fn = os.path.join(doc_dir, 'index.html')
306
- if not os.path.exists(fn):
307
- raise DistlibException('not found: %r' % fn)
308
- metadata.validate()
309
- name, version = metadata.name, metadata.version
310
- zip_data = zip_dir(doc_dir).getvalue()
311
- fields = [(':action', 'doc_upload'),
312
- ('name', name), ('version', version)]
313
- files = [('content', name, zip_data)]
314
- request = self.encode_request(fields, files)
315
- return self.send_request(request)
316
-
317
- def get_verify_command(self, signature_filename, data_filename,
318
- keystore=None):
319
- """
320
- Return a suitable command for verifying a file.
321
-
322
- :param signature_filename: The pathname to the file containing the
323
- signature.
324
- :param data_filename: The pathname to the file containing the
325
- signed data.
326
- :param keystore: The path to a directory which contains the keys
327
- used in verification. If not specified, the
328
- instance's ``gpg_home`` attribute is used instead.
329
- :return: The verifying command as a list suitable to be
330
- passed to :class:`subprocess.Popen`.
331
- """
332
- cmd = [self.gpg, '--status-fd', '2', '--no-tty']
333
- if keystore is None:
334
- keystore = self.gpg_home
335
- if keystore:
336
- cmd.extend(['--homedir', keystore])
337
- cmd.extend(['--verify', signature_filename, data_filename])
338
- logger.debug('invoking: %s', ' '.join(cmd))
339
- return cmd
340
-
341
- def verify_signature(self, signature_filename, data_filename,
342
- keystore=None):
343
- """
344
- Verify a signature for a file.
345
-
346
- :param signature_filename: The pathname to the file containing the
347
- signature.
348
- :param data_filename: The pathname to the file containing the
349
- signed data.
350
- :param keystore: The path to a directory which contains the keys
351
- used in verification. If not specified, the
352
- instance's ``gpg_home`` attribute is used instead.
353
- :return: True if the signature was verified, else False.
354
- """
355
- if not self.gpg:
356
- raise DistlibException('verification unavailable because gpg '
357
- 'unavailable')
358
- cmd = self.get_verify_command(signature_filename, data_filename,
359
- keystore)
360
- rc, stdout, stderr = self.run_command(cmd)
361
- if rc not in (0, 1):
362
- raise DistlibException('verify command failed with error code %s' % rc)
363
- return rc == 0
364
-
365
- def download_file(self, url, destfile, digest=None, reporthook=None):
366
- """
367
- This is a convenience method for downloading a file from an URL.
368
- Normally, this will be a file from the index, though currently
369
- no check is made for this (i.e. a file can be downloaded from
370
- anywhere).
371
-
372
- The method is just like the :func:`urlretrieve` function in the
373
- standard library, except that it allows digest computation to be
374
- done during download and checking that the downloaded data
375
- matched any expected value.
376
-
377
- :param url: The URL of the file to be downloaded (assumed to be
378
- available via an HTTP GET request).
379
- :param destfile: The pathname where the downloaded file is to be
380
- saved.
381
- :param digest: If specified, this must be a (hasher, value)
382
- tuple, where hasher is the algorithm used (e.g.
383
- ``'md5'``) and ``value`` is the expected value.
384
- :param reporthook: The same as for :func:`urlretrieve` in the
385
- standard library.
386
- """
387
- if digest is None:
388
- digester = None
389
- logger.debug('No digest specified')
390
- else:
391
- if isinstance(digest, (list, tuple)):
392
- hasher, digest = digest
393
- else:
394
- hasher = 'md5'
395
- digester = getattr(hashlib, hasher)()
396
- logger.debug('Digest specified: %s' % digest)
397
- # The following code is equivalent to urlretrieve.
398
- # We need to do it this way so that we can compute the
399
- # digest of the file as we go.
400
- with open(destfile, 'wb') as dfp:
401
- # addinfourl is not a context manager on 2.x
402
- # so we have to use try/finally
403
- sfp = self.send_request(Request(url))
404
- try:
405
- headers = sfp.info()
406
- blocksize = 8192
407
- size = -1
408
- read = 0
409
- blocknum = 0
410
- if "content-length" in headers:
411
- size = int(headers["Content-Length"])
412
- if reporthook:
413
- reporthook(blocknum, blocksize, size)
414
- while True:
415
- block = sfp.read(blocksize)
416
- if not block:
417
- break
418
- read += len(block)
419
- dfp.write(block)
420
- if digester:
421
- digester.update(block)
422
- blocknum += 1
423
- if reporthook:
424
- reporthook(blocknum, blocksize, size)
425
- finally:
426
- sfp.close()
427
-
428
- # check that we got the whole file, if we can
429
- if size >= 0 and read < size:
430
- raise DistlibException(
431
- 'retrieval incomplete: got only %d out of %d bytes'
432
- % (read, size))
433
- # if we have a digest, it must match.
434
- if digester:
435
- actual = digester.hexdigest()
436
- if digest != actual:
437
- raise DistlibException('%s digest mismatch for %s: expected '
438
- '%s, got %s' % (hasher, destfile,
439
- digest, actual))
440
- logger.debug('Digest verified: %s', digest)
441
-
442
- def send_request(self, req):
443
- """
444
- Send a standard library :class:`Request` to PyPI and return its
445
- response.
446
-
447
- :param req: The request to send.
448
- :return: The HTTP response from PyPI (a standard library HTTPResponse).
449
- """
450
- handlers = []
451
- if self.password_handler:
452
- handlers.append(self.password_handler)
453
- if self.ssl_verifier:
454
- handlers.append(self.ssl_verifier)
455
- opener = build_opener(*handlers)
456
- return opener.open(req)
457
-
458
- def encode_request(self, fields, files):
459
- """
460
- Encode fields and files for posting to an HTTP server.
461
-
462
- :param fields: The fields to send as a list of (fieldname, value)
463
- tuples.
464
- :param files: The files to send as a list of (fieldname, filename,
465
- file_bytes) tuple.
466
- """
467
- # Adapted from packaging, which in turn was adapted from
468
- # http://code.activestate.com/recipes/146306
469
-
470
- parts = []
471
- boundary = self.boundary
472
- for k, values in fields:
473
- if not isinstance(values, (list, tuple)):
474
- values = [values]
475
-
476
- for v in values:
477
- parts.extend((
478
- b'--' + boundary,
479
- ('Content-Disposition: form-data; name="%s"' %
480
- k).encode('utf-8'),
481
- b'',
482
- v.encode('utf-8')))
483
- for key, filename, value in files:
484
- parts.extend((
485
- b'--' + boundary,
486
- ('Content-Disposition: form-data; name="%s"; filename="%s"' %
487
- (key, filename)).encode('utf-8'),
488
- b'',
489
- value))
490
-
491
- parts.extend((b'--' + boundary + b'--', b''))
492
-
493
- body = b'\r\n'.join(parts)
494
- ct = b'multipart/form-data; boundary=' + boundary
495
- headers = {
496
- 'Content-type': ct,
497
- 'Content-length': str(len(body))
498
- }
499
- return Request(self.url, body, headers)
500
-
501
- def search(self, terms, operator=None): # pragma: no cover
502
- if isinstance(terms, string_types):
503
- terms = {'name': terms}
504
- rpc_proxy = ServerProxy(self.url, timeout=3.0)
505
- try:
506
- return rpc_proxy.search(terms, operator or 'and')
507
- finally:
508
- rpc_proxy('close')()