ยปCore Development>Code coverage>Lib/packaging/pypi/dist.py

Python code coverage for Lib/packaging/pypi/dist.py

#countcontent
1n/a"""Classes representing releases and distributions retrieved from indexes.
2n/a
3n/aA project (= unique name) can have several releases (= versions) and
4n/aeach release can have several distributions (= sdist and bdists).
5n/a
6n/aRelease objects contain metadata-related information (see PEP 376);
7n/adistribution objects contain download-related information.
8n/a"""
9n/a
10n/aimport re
11n/aimport hashlib
12n/aimport tempfile
13n/aimport urllib.request
14n/aimport urllib.parse
15n/aimport urllib.error
16n/aimport urllib.parse
17n/afrom shutil import unpack_archive
18n/a
19n/afrom packaging.errors import IrrationalVersionError
20n/afrom packaging.version import (suggest_normalized_version, NormalizedVersion,
21n/a get_version_predicate)
22n/afrom packaging.metadata import Metadata
23n/afrom packaging.pypi.errors import (HashDoesNotMatch, UnsupportedHashName,
24n/a CantParseArchiveName)
25n/a
26n/a
27n/a__all__ = ['ReleaseInfo', 'DistInfo', 'ReleasesList', 'get_infos_from_url']
28n/a
29n/aEXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz .egg".split()
30n/aMD5_HASH = re.compile(r'^.*#md5=([a-f0-9]+)$')
31n/aDIST_TYPES = ['bdist', 'sdist']
32n/a
33n/a
34n/aclass IndexReference:
35n/a """Mixin used to store the index reference"""
36n/a def set_index(self, index=None):
37n/a self._index = index
38n/a
39n/a
40n/aclass ReleaseInfo(IndexReference):
41n/a """Represent a release of a project (a project with a specific version).
42n/a The release contain the _metadata informations related to this specific
43n/a version, and is also a container for distribution related informations.
44n/a
45n/a See the DistInfo class for more information about distributions.
46n/a """
47n/a
48n/a def __init__(self, name, version, metadata=None, hidden=False,
49n/a index=None, **kwargs):
50n/a """
51n/a :param name: the name of the distribution
52n/a :param version: the version of the distribution
53n/a :param metadata: the metadata fields of the release.
54n/a :type metadata: dict
55n/a :param kwargs: optional arguments for a new distribution.
56n/a """
57n/a self.set_index(index)
58n/a self.name = name
59n/a self._version = None
60n/a self.version = version
61n/a if metadata:
62n/a self.metadata = Metadata(mapping=metadata)
63n/a else:
64n/a self.metadata = None
65n/a self.dists = {}
66n/a self.hidden = hidden
67n/a
68n/a if 'dist_type' in kwargs:
69n/a dist_type = kwargs.pop('dist_type')
70n/a self.add_distribution(dist_type, **kwargs)
71n/a
72n/a def set_version(self, version):
73n/a try:
74n/a self._version = NormalizedVersion(version)
75n/a except IrrationalVersionError:
76n/a suggestion = suggest_normalized_version(version)
77n/a if suggestion:
78n/a self.version = suggestion
79n/a else:
80n/a raise IrrationalVersionError(version)
81n/a
82n/a def get_version(self):
83n/a return self._version
84n/a
85n/a version = property(get_version, set_version)
86n/a
87n/a def fetch_metadata(self):
88n/a """If the metadata is not set, use the indexes to get it"""
89n/a if not self.metadata:
90n/a self._index.get_metadata(self.name, str(self.version))
91n/a return self.metadata
92n/a
93n/a @property
94n/a def is_final(self):
95n/a """proxy to version.is_final"""
96n/a return self.version.is_final
97n/a
98n/a def fetch_distributions(self):
99n/a if self.dists is None:
100n/a self._index.get_distributions(self.name, str(self.version))
101n/a if self.dists is None:
102n/a self.dists = {}
103n/a return self.dists
104n/a
105n/a def add_distribution(self, dist_type='sdist', python_version=None,
106n/a **params):
107n/a """Add distribution informations to this release.
108n/a If distribution information is already set for this distribution type,
109n/a add the given url paths to the distribution. This can be useful while
110n/a some of them fails to download.
111n/a
112n/a :param dist_type: the distribution type (eg. "sdist", "bdist", etc.)
113n/a :param params: the fields to be passed to the distribution object
114n/a (see the :class:DistInfo constructor).
115n/a """
116n/a if dist_type not in DIST_TYPES:
117n/a raise ValueError(dist_type)
118n/a if dist_type in self.dists:
119n/a self.dists[dist_type].add_url(**params)
120n/a else:
121n/a self.dists[dist_type] = DistInfo(self, dist_type,
122n/a index=self._index, **params)
123n/a if python_version:
124n/a self.dists[dist_type].python_version = python_version
125n/a
126n/a def get_distribution(self, dist_type=None, prefer_source=True):
127n/a """Return a distribution.
128n/a
129n/a If dist_type is set, find first for this distribution type, and just
130n/a act as an alias of __get_item__.
131n/a
132n/a If prefer_source is True, search first for source distribution, and if
133n/a not return one existing distribution.
134n/a """
135n/a if len(self.dists) == 0:
136n/a raise LookupError
137n/a if dist_type:
138n/a return self[dist_type]
139n/a if prefer_source:
140n/a if "sdist" in self.dists:
141n/a dist = self["sdist"]
142n/a else:
143n/a dist = next(self.dists.values())
144n/a return dist
145n/a
146n/a def unpack(self, path=None, prefer_source=True):
147n/a """Unpack the distribution to the given path.
148n/a
149n/a If not destination is given, creates a temporary location.
150n/a
151n/a Returns the location of the extracted files (root).
152n/a """
153n/a return self.get_distribution(prefer_source=prefer_source)\
154n/a .unpack(path=path)
155n/a
156n/a def download(self, temp_path=None, prefer_source=True):
157n/a """Download the distribution, using the requirements.
158n/a
159n/a If more than one distribution match the requirements, use the last
160n/a version.
161n/a Download the distribution, and put it in the temp_path. If no temp_path
162n/a is given, creates and return one.
163n/a
164n/a Returns the complete absolute path to the downloaded archive.
165n/a """
166n/a return self.get_distribution(prefer_source=prefer_source)\
167n/a .download(path=temp_path)
168n/a
169n/a def set_metadata(self, metadata):
170n/a if not self.metadata:
171n/a self.metadata = Metadata()
172n/a self.metadata.update(metadata)
173n/a
174n/a def __getitem__(self, item):
175n/a """distributions are available using release["sdist"]"""
176n/a return self.dists[item]
177n/a
178n/a def _check_is_comparable(self, other):
179n/a if not isinstance(other, ReleaseInfo):
180n/a raise TypeError("cannot compare %s and %s"
181n/a % (type(self).__name__, type(other).__name__))
182n/a elif self.name != other.name:
183n/a raise TypeError("cannot compare %s and %s"
184n/a % (self.name, other.name))
185n/a
186n/a def __repr__(self):
187n/a return "<%s %s>" % (self.name, self.version)
188n/a
189n/a def __eq__(self, other):
190n/a self._check_is_comparable(other)
191n/a return self.version == other.version
192n/a
193n/a def __lt__(self, other):
194n/a self._check_is_comparable(other)
195n/a return self.version < other.version
196n/a
197n/a def __ne__(self, other):
198n/a return not self.__eq__(other)
199n/a
200n/a def __gt__(self, other):
201n/a return not (self.__lt__(other) or self.__eq__(other))
202n/a
203n/a def __le__(self, other):
204n/a return self.__eq__(other) or self.__lt__(other)
205n/a
206n/a def __ge__(self, other):
207n/a return self.__eq__(other) or self.__gt__(other)
208n/a
209n/a # See http://docs.python.org/reference/datamodel#object.__hash__
210n/a __hash__ = object.__hash__
211n/a
212n/a
213n/aclass DistInfo(IndexReference):
214n/a """Represents a distribution retrieved from an index (sdist, bdist, ...)
215n/a """
216n/a
217n/a def __init__(self, release, dist_type=None, url=None, hashname=None,
218n/a hashval=None, is_external=True, python_version=None,
219n/a index=None):
220n/a """Create a new instance of DistInfo.
221n/a
222n/a :param release: a DistInfo class is relative to a release.
223n/a :param dist_type: the type of the dist (eg. source, bin-*, etc.)
224n/a :param url: URL where we found this distribution
225n/a :param hashname: the name of the hash we want to use. Refer to the
226n/a hashlib.new documentation for more information.
227n/a :param hashval: the hash value.
228n/a :param is_external: we need to know if the provided url comes from
229n/a an index browsing, or from an external resource.
230n/a
231n/a """
232n/a self.set_index(index)
233n/a self.release = release
234n/a self.dist_type = dist_type
235n/a self.python_version = python_version
236n/a self._unpacked_dir = None
237n/a # set the downloaded path to None by default. The goal here
238n/a # is to not download distributions multiple times
239n/a self.downloaded_location = None
240n/a # We store urls in dict, because we need to have a bit more infos
241n/a # than the simple URL. It will be used later to find the good url to
242n/a # use.
243n/a # We have two _url* attributes: _url and urls. urls contains a list
244n/a # of dict for the different urls, and _url contains the choosen url, in
245n/a # order to dont make the selection process multiple times.
246n/a self.urls = []
247n/a self._url = None
248n/a self.add_url(url, hashname, hashval, is_external)
249n/a
250n/a def add_url(self, url=None, hashname=None, hashval=None, is_external=True):
251n/a """Add a new url to the list of urls"""
252n/a if hashname is not None:
253n/a try:
254n/a hashlib.new(hashname)
255n/a except ValueError:
256n/a raise UnsupportedHashName(hashname)
257n/a if url not in [u['url'] for u in self.urls]:
258n/a self.urls.append({
259n/a 'url': url,
260n/a 'hashname': hashname,
261n/a 'hashval': hashval,
262n/a 'is_external': is_external,
263n/a })
264n/a # reset the url selection process
265n/a self._url = None
266n/a
267n/a @property
268n/a def url(self):
269n/a """Pick up the right url for the list of urls in self.urls"""
270n/a # We return internal urls over externals.
271n/a # If there is more than one internal or external, return the first
272n/a # one.
273n/a if self._url is None:
274n/a if len(self.urls) > 1:
275n/a internals_urls = [u for u in self.urls \
276n/a if u['is_external'] == False]
277n/a if len(internals_urls) >= 1:
278n/a self._url = internals_urls[0]
279n/a if self._url is None:
280n/a self._url = self.urls[0]
281n/a return self._url
282n/a
283n/a @property
284n/a def is_source(self):
285n/a """return if the distribution is a source one or not"""
286n/a return self.dist_type == 'sdist'
287n/a
288n/a def download(self, path=None):
289n/a """Download the distribution to a path, and return it.
290n/a
291n/a If the path is given in path, use this, otherwise, generates a new one
292n/a Return the download location.
293n/a """
294n/a if path is None:
295n/a path = tempfile.mkdtemp()
296n/a
297n/a # if we do not have downloaded it yet, do it.
298n/a if self.downloaded_location is None:
299n/a url = self.url['url']
300n/a archive_name = urllib.parse.urlparse(url)[2].split('/')[-1]
301n/a filename, headers = urllib.request.urlretrieve(url,
302n/a path + "/" + archive_name)
303n/a self.downloaded_location = filename
304n/a self._check_md5(filename)
305n/a return self.downloaded_location
306n/a
307n/a def unpack(self, path=None):
308n/a """Unpack the distribution to the given path.
309n/a
310n/a If not destination is given, creates a temporary location.
311n/a
312n/a Returns the location of the extracted files (root).
313n/a """
314n/a if not self._unpacked_dir:
315n/a if path is None:
316n/a path = tempfile.mkdtemp()
317n/a
318n/a filename = self.download(path)
319n/a unpack_archive(filename, path)
320n/a self._unpacked_dir = path
321n/a
322n/a return path
323n/a
324n/a def _check_md5(self, filename):
325n/a """Check that the md5 checksum of the given file matches the one in
326n/a url param"""
327n/a hashname = self.url['hashname']
328n/a expected_hashval = self.url['hashval']
329n/a if None not in (expected_hashval, hashname):
330n/a with open(filename, 'rb') as f:
331n/a hashval = hashlib.new(hashname)
332n/a hashval.update(f.read())
333n/a
334n/a if hashval.hexdigest() != expected_hashval:
335n/a raise HashDoesNotMatch("got %s instead of %s"
336n/a % (hashval.hexdigest(), expected_hashval))
337n/a
338n/a def __repr__(self):
339n/a if self.release is None:
340n/a return "<? ? %s>" % self.dist_type
341n/a
342n/a return "<%s %s %s>" % (
343n/a self.release.name, self.release.version, self.dist_type or "")
344n/a
345n/a
346n/aclass ReleasesList(IndexReference):
347n/a """A container of Release.
348n/a
349n/a Provides useful methods and facilities to sort and filter releases.
350n/a """
351n/a def __init__(self, name, releases=None, contains_hidden=False, index=None):
352n/a self.set_index(index)
353n/a self.releases = []
354n/a self.name = name
355n/a self.contains_hidden = contains_hidden
356n/a if releases:
357n/a self.add_releases(releases)
358n/a
359n/a def fetch_releases(self):
360n/a self._index.get_releases(self.name)
361n/a return self.releases
362n/a
363n/a def filter(self, predicate):
364n/a """Filter and return a subset of releases matching the given predicate.
365n/a """
366n/a return ReleasesList(self.name, [release for release in self.releases
367n/a if predicate.match(release.version)],
368n/a index=self._index)
369n/a
370n/a def get_last(self, requirements, prefer_final=None):
371n/a """Return the "last" release, that satisfy the given predicates.
372n/a
373n/a "last" is defined by the version number of the releases, you also could
374n/a set prefer_final parameter to True or False to change the order results
375n/a """
376n/a predicate = get_version_predicate(requirements)
377n/a releases = self.filter(predicate)
378n/a if len(releases) == 0:
379n/a return None
380n/a releases.sort_releases(prefer_final, reverse=True)
381n/a return releases[0]
382n/a
383n/a def add_releases(self, releases):
384n/a """Add releases in the release list.
385n/a
386n/a :param: releases is a list of ReleaseInfo objects.
387n/a """
388n/a for r in releases:
389n/a self.add_release(release=r)
390n/a
391n/a def add_release(self, version=None, dist_type='sdist', release=None,
392n/a **dist_args):
393n/a """Add a release to the list.
394n/a
395n/a The release can be passed in the `release` parameter, and in this case,
396n/a it will be crawled to extract the useful informations if necessary, or
397n/a the release informations can be directly passed in the `version` and
398n/a `dist_type` arguments.
399n/a
400n/a Other keywords arguments can be provided, and will be forwarded to the
401n/a distribution creation (eg. the arguments of the DistInfo constructor).
402n/a """
403n/a if release:
404n/a if release.name.lower() != self.name.lower():
405n/a raise ValueError("%s is not the same project as %s" %
406n/a (release.name, self.name))
407n/a version = str(release.version)
408n/a
409n/a if version not in self.get_versions():
410n/a # append only if not already exists
411n/a self.releases.append(release)
412n/a for dist in release.dists.values():
413n/a for url in dist.urls:
414n/a self.add_release(version, dist.dist_type, **url)
415n/a else:
416n/a matches = [r for r in self.releases
417n/a if str(r.version) == version and r.name == self.name]
418n/a if not matches:
419n/a release = ReleaseInfo(self.name, version, index=self._index)
420n/a self.releases.append(release)
421n/a else:
422n/a release = matches[0]
423n/a
424n/a release.add_distribution(dist_type=dist_type, **dist_args)
425n/a
426n/a def sort_releases(self, prefer_final=False, reverse=True, *args, **kwargs):
427n/a """Sort the results with the given properties.
428n/a
429n/a The `prefer_final` argument can be used to specify if final
430n/a distributions (eg. not dev, beta or alpha) would be preferred or not.
431n/a
432n/a Results can be inverted by using `reverse`.
433n/a
434n/a Any other parameter provided will be forwarded to the sorted call. You
435n/a cannot redefine the key argument of "sorted" here, as it is used
436n/a internally to sort the releases.
437n/a """
438n/a
439n/a sort_by = []
440n/a if prefer_final:
441n/a sort_by.append("is_final")
442n/a sort_by.append("version")
443n/a
444n/a self.releases.sort(
445n/a key=lambda i: tuple(getattr(i, arg) for arg in sort_by),
446n/a reverse=reverse, *args, **kwargs)
447n/a
448n/a def get_release(self, version):
449n/a """Return a release from its version."""
450n/a matches = [r for r in self.releases if str(r.version) == version]
451n/a if len(matches) != 1:
452n/a raise KeyError(version)
453n/a return matches[0]
454n/a
455n/a def get_versions(self):
456n/a """Return a list of releases versions contained"""
457n/a return [str(r.version) for r in self.releases]
458n/a
459n/a def __getitem__(self, key):
460n/a return self.releases[key]
461n/a
462n/a def __len__(self):
463n/a return len(self.releases)
464n/a
465n/a def __repr__(self):
466n/a string = 'Project "%s"' % self.name
467n/a if self.get_versions():
468n/a string += ' versions: %s' % ', '.join(self.get_versions())
469n/a return '<%s>' % string
470n/a
471n/a
472n/adef get_infos_from_url(url, probable_dist_name=None, is_external=True):
473n/a """Get useful informations from an URL.
474n/a
475n/a Return a dict of (name, version, url, hashtype, hash, is_external)
476n/a
477n/a :param url: complete url of the distribution
478n/a :param probable_dist_name: A probable name of the project.
479n/a :param is_external: Tell if the url commes from an index or from
480n/a an external URL.
481n/a """
482n/a # if the url contains a md5 hash, get it.
483n/a md5_hash = None
484n/a match = MD5_HASH.match(url)
485n/a if match is not None:
486n/a md5_hash = match.group(1)
487n/a # remove the hash
488n/a url = url.replace("#md5=%s" % md5_hash, "")
489n/a
490n/a # parse the archive name to find dist name and version
491n/a archive_name = urllib.parse.urlparse(url)[2].split('/')[-1]
492n/a extension_matched = False
493n/a # remove the extension from the name
494n/a for ext in EXTENSIONS:
495n/a if archive_name.endswith(ext):
496n/a archive_name = archive_name[:-len(ext)]
497n/a extension_matched = True
498n/a
499n/a name, version = split_archive_name(archive_name)
500n/a if extension_matched is True:
501n/a return {'name': name,
502n/a 'version': version,
503n/a 'url': url,
504n/a 'hashname': "md5",
505n/a 'hashval': md5_hash,
506n/a 'is_external': is_external,
507n/a 'dist_type': 'sdist'}
508n/a
509n/a
510n/adef split_archive_name(archive_name, probable_name=None):
511n/a """Split an archive name into two parts: name and version.
512n/a
513n/a Return the tuple (name, version)
514n/a """
515n/a # Try to determine wich part is the name and wich is the version using the
516n/a # "-" separator. Take the larger part to be the version number then reduce
517n/a # if this not works.
518n/a def eager_split(str, maxsplit=2):
519n/a # split using the "-" separator
520n/a splits = str.rsplit("-", maxsplit)
521n/a name = splits[0]
522n/a version = "-".join(splits[1:])
523n/a if version.startswith("-"):
524n/a version = version[1:]
525n/a if suggest_normalized_version(version) is None and maxsplit >= 0:
526n/a # we dont get a good version number: recurse !
527n/a return eager_split(str, maxsplit - 1)
528n/a else:
529n/a return name, version
530n/a if probable_name is not None:
531n/a probable_name = probable_name.lower()
532n/a name = None
533n/a if probable_name is not None and probable_name in archive_name:
534n/a # we get the name from probable_name, if given.
535n/a name = probable_name
536n/a version = archive_name.lstrip(name)
537n/a else:
538n/a name, version = eager_split(archive_name)
539n/a
540n/a version = suggest_normalized_version(version)
541n/a if version is not None and name != "":
542n/a return name.lower(), version
543n/a else:
544n/a raise CantParseArchiveName(archive_name)