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

Python code coverage for Lib/packaging/version.py

#countcontent
1n/a"""Implementation of the versioning scheme defined in PEP 386."""
2n/a
3n/aimport re
4n/a
5n/afrom packaging.errors import IrrationalVersionError, HugeMajorVersionNumError
6n/a
7n/a__all__ = ['NormalizedVersion', 'suggest_normalized_version',
8n/a 'VersionPredicate', 'is_valid_version', 'is_valid_versions',
9n/a 'is_valid_predicate']
10n/a
11n/a# A marker used in the second and third parts of the `parts` tuple, for
12n/a# versions that don't have those segments, to sort properly. An example
13n/a# of versions in sort order ('highest' last):
14n/a# 1.0b1 ((1,0), ('b',1), ('z',))
15n/a# 1.0.dev345 ((1,0), ('z',), ('dev', 345))
16n/a# 1.0 ((1,0), ('z',), ('z',))
17n/a# 1.0.post256.dev345 ((1,0), ('z',), ('z', 'post', 256, 'dev', 345))
18n/a# 1.0.post345 ((1,0), ('z',), ('z', 'post', 345, 'z'))
19n/a# ^ ^ ^
20n/a# 'b' < 'z' ---------------------/ | |
21n/a# | |
22n/a# 'dev' < 'z' ----------------------------/ |
23n/a# |
24n/a# 'dev' < 'z' ----------------------------------------------/
25n/a# 'f' for 'final' would be kind of nice, but due to bugs in the support of
26n/a# 'rc' we must use 'z'
27n/a_FINAL_MARKER = ('z',)
28n/a
29n/a_VERSION_RE = re.compile(r'''
30n/a ^
31n/a (?P<version>\d+\.\d+) # minimum 'N.N'
32n/a (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
33n/a (?:
34n/a (?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate
35n/a # 'rc'= alias for release candidate
36n/a (?P<prerelversion>\d+(?:\.\d+)*)
37n/a )?
38n/a (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
39n/a $''', re.VERBOSE)
40n/a
41n/a
42n/aclass NormalizedVersion:
43n/a """A rational version.
44n/a
45n/a Good:
46n/a 1.2 # equivalent to "1.2.0"
47n/a 1.2.0
48n/a 1.2a1
49n/a 1.2.3a2
50n/a 1.2.3b1
51n/a 1.2.3c1
52n/a 1.2.3.4
53n/a TODO: fill this out
54n/a
55n/a Bad:
56n/a 1 # mininum two numbers
57n/a 1.2a # release level must have a release serial
58n/a 1.2.3b
59n/a """
60n/a def __init__(self, s, error_on_huge_major_num=True):
61n/a """Create a NormalizedVersion instance from a version string.
62n/a
63n/a @param s {str} The version string.
64n/a @param error_on_huge_major_num {bool} Whether to consider an
65n/a apparent use of a year or full date as the major version number
66n/a an error. Default True. One of the observed patterns on PyPI before
67n/a the introduction of `NormalizedVersion` was version numbers like
68n/a this:
69n/a 2009.01.03
70n/a 20040603
71n/a 2005.01
72n/a This guard is here to strongly encourage the package author to
73n/a use an alternate version, because a release deployed into PyPI
74n/a and, e.g. downstream Linux package managers, will forever remove
75n/a the possibility of using a version number like "1.0" (i.e.
76n/a where the major number is less than that huge major number).
77n/a """
78n/a self.is_final = True # by default, consider a version as final.
79n/a self._parse(s, error_on_huge_major_num)
80n/a
81n/a @classmethod
82n/a def from_parts(cls, version, prerelease=_FINAL_MARKER,
83n/a devpost=_FINAL_MARKER):
84n/a return cls(cls.parts_to_str((version, prerelease, devpost)))
85n/a
86n/a def _parse(self, s, error_on_huge_major_num=True):
87n/a """Parses a string version into parts."""
88n/a match = _VERSION_RE.search(s)
89n/a if not match:
90n/a raise IrrationalVersionError(s)
91n/a
92n/a groups = match.groupdict()
93n/a parts = []
94n/a
95n/a # main version
96n/a block = self._parse_numdots(groups['version'], s, False, 2)
97n/a extraversion = groups.get('extraversion')
98n/a if extraversion not in ('', None):
99n/a block += self._parse_numdots(extraversion[1:], s)
100n/a parts.append(tuple(block))
101n/a
102n/a # prerelease
103n/a prerel = groups.get('prerel')
104n/a if prerel is not None:
105n/a block = [prerel]
106n/a block += self._parse_numdots(groups.get('prerelversion'), s,
107n/a pad_zeros_length=1)
108n/a parts.append(tuple(block))
109n/a self.is_final = False
110n/a else:
111n/a parts.append(_FINAL_MARKER)
112n/a
113n/a # postdev
114n/a if groups.get('postdev'):
115n/a post = groups.get('post')
116n/a dev = groups.get('dev')
117n/a postdev = []
118n/a if post is not None:
119n/a postdev.extend((_FINAL_MARKER[0], 'post', int(post)))
120n/a if dev is None:
121n/a postdev.append(_FINAL_MARKER[0])
122n/a if dev is not None:
123n/a postdev.extend(('dev', int(dev)))
124n/a self.is_final = False
125n/a parts.append(tuple(postdev))
126n/a else:
127n/a parts.append(_FINAL_MARKER)
128n/a self.parts = tuple(parts)
129n/a if error_on_huge_major_num and self.parts[0][0] > 1980:
130n/a raise HugeMajorVersionNumError("huge major version number, %r, "
131n/a "which might cause future problems: %r" % (self.parts[0][0], s))
132n/a
133n/a def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
134n/a pad_zeros_length=0):
135n/a """Parse 'N.N.N' sequences, return a list of ints.
136n/a
137n/a @param s {str} 'N.N.N...' sequence to be parsed
138n/a @param full_ver_str {str} The full version string from which this
139n/a comes. Used for error strings.
140n/a @param drop_trailing_zeros {bool} Whether to drop trailing zeros
141n/a from the returned list. Default True.
142n/a @param pad_zeros_length {int} The length to which to pad the
143n/a returned list with zeros, if necessary. Default 0.
144n/a """
145n/a nums = []
146n/a for n in s.split("."):
147n/a if len(n) > 1 and n[0] == '0':
148n/a raise IrrationalVersionError("cannot have leading zero in "
149n/a "version number segment: '%s' in %r" % (n, full_ver_str))
150n/a nums.append(int(n))
151n/a if drop_trailing_zeros:
152n/a while nums and nums[-1] == 0:
153n/a nums.pop()
154n/a while len(nums) < pad_zeros_length:
155n/a nums.append(0)
156n/a return nums
157n/a
158n/a def __str__(self):
159n/a return self.parts_to_str(self.parts)
160n/a
161n/a @classmethod
162n/a def parts_to_str(cls, parts):
163n/a """Transforms a version expressed in tuple into its string
164n/a representation."""
165n/a # XXX This doesn't check for invalid tuples
166n/a main, prerel, postdev = parts
167n/a s = '.'.join(str(v) for v in main)
168n/a if prerel is not _FINAL_MARKER:
169n/a s += prerel[0]
170n/a s += '.'.join(str(v) for v in prerel[1:])
171n/a # XXX clean up: postdev is always true; code is obscure
172n/a if postdev and postdev is not _FINAL_MARKER:
173n/a if postdev[0] == _FINAL_MARKER[0]:
174n/a postdev = postdev[1:]
175n/a i = 0
176n/a while i < len(postdev):
177n/a if i % 2 == 0:
178n/a s += '.'
179n/a s += str(postdev[i])
180n/a i += 1
181n/a return s
182n/a
183n/a def __repr__(self):
184n/a return "%s('%s')" % (self.__class__.__name__, self)
185n/a
186n/a def _cannot_compare(self, other):
187n/a raise TypeError("cannot compare %s and %s"
188n/a % (type(self).__name__, type(other).__name__))
189n/a
190n/a def __eq__(self, other):
191n/a if not isinstance(other, NormalizedVersion):
192n/a self._cannot_compare(other)
193n/a return self.parts == other.parts
194n/a
195n/a def __lt__(self, other):
196n/a if not isinstance(other, NormalizedVersion):
197n/a self._cannot_compare(other)
198n/a return self.parts < other.parts
199n/a
200n/a def __ne__(self, other):
201n/a return not self.__eq__(other)
202n/a
203n/a def __gt__(self, other):
204n/a return not (self.__lt__(other) or self.__eq__(other))
205n/a
206n/a def __le__(self, other):
207n/a return self.__eq__(other) or self.__lt__(other)
208n/a
209n/a def __ge__(self, other):
210n/a return self.__eq__(other) or self.__gt__(other)
211n/a
212n/a # See http://docs.python.org/reference/datamodel#object.__hash__
213n/a def __hash__(self):
214n/a return hash(self.parts)
215n/a
216n/a
217n/adef suggest_normalized_version(s):
218n/a """Suggest a normalized version close to the given version string.
219n/a
220n/a If you have a version string that isn't rational (i.e. NormalizedVersion
221n/a doesn't like it) then you might be able to get an equivalent (or close)
222n/a rational version from this function.
223n/a
224n/a This does a number of simple normalizations to the given string, based
225n/a on observation of versions currently in use on PyPI. Given a dump of
226n/a those version during PyCon 2009, 4287 of them:
227n/a - 2312 (53.93%) match NormalizedVersion without change
228n/a with the automatic suggestion
229n/a - 3474 (81.04%) match when using this suggestion method
230n/a
231n/a @param s {str} An irrational version string.
232n/a @returns A rational version string, or None, if couldn't determine one.
233n/a """
234n/a try:
235n/a NormalizedVersion(s)
236n/a return s # already rational
237n/a except IrrationalVersionError:
238n/a pass
239n/a
240n/a rs = s.lower()
241n/a
242n/a # part of this could use maketrans
243n/a for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
244n/a ('beta', 'b'), ('rc', 'c'), ('-final', ''),
245n/a ('-pre', 'c'),
246n/a ('-release', ''), ('.release', ''), ('-stable', ''),
247n/a ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
248n/a ('final', '')):
249n/a rs = rs.replace(orig, repl)
250n/a
251n/a # if something ends with dev or pre, we add a 0
252n/a rs = re.sub(r"pre$", r"pre0", rs)
253n/a rs = re.sub(r"dev$", r"dev0", rs)
254n/a
255n/a # if we have something like "b-2" or "a.2" at the end of the
256n/a # version, that is pobably beta, alpha, etc
257n/a # let's remove the dash or dot
258n/a rs = re.sub(r"([abc]|rc)[\-\.](\d+)$", r"\1\2", rs)
259n/a
260n/a # 1.0-dev-r371 -> 1.0.dev371
261n/a # 0.1-dev-r79 -> 0.1.dev79
262n/a rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
263n/a
264n/a # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
265n/a rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
266n/a
267n/a # Clean: v0.3, v1.0
268n/a if rs.startswith('v'):
269n/a rs = rs[1:]
270n/a
271n/a # Clean leading '0's on numbers.
272n/a #TODO: unintended side-effect on, e.g., "2003.05.09"
273n/a # PyPI stats: 77 (~2%) better
274n/a rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
275n/a
276n/a # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
277n/a # zero.
278n/a # PyPI stats: 245 (7.56%) better
279n/a rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
280n/a
281n/a # the 'dev-rNNN' tag is a dev tag
282n/a rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
283n/a
284n/a # clean the - when used as a pre delimiter
285n/a rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
286n/a
287n/a # a terminal "dev" or "devel" can be changed into ".dev0"
288n/a rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
289n/a
290n/a # a terminal "dev" can be changed into ".dev0"
291n/a rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
292n/a
293n/a # a terminal "final" or "stable" can be removed
294n/a rs = re.sub(r"(final|stable)$", "", rs)
295n/a
296n/a # The 'r' and the '-' tags are post release tags
297n/a # 0.4a1.r10 -> 0.4a1.post10
298n/a # 0.9.33-17222 -> 0.9.33.post17222
299n/a # 0.9.33-r17222 -> 0.9.33.post17222
300n/a rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
301n/a
302n/a # Clean 'r' instead of 'dev' usage:
303n/a # 0.9.33+r17222 -> 0.9.33.dev17222
304n/a # 1.0dev123 -> 1.0.dev123
305n/a # 1.0.git123 -> 1.0.dev123
306n/a # 1.0.bzr123 -> 1.0.dev123
307n/a # 0.1a0dev.123 -> 0.1a0.dev123
308n/a # PyPI stats: ~150 (~4%) better
309n/a rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
310n/a
311n/a # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
312n/a # 0.2.pre1 -> 0.2c1
313n/a # 0.2-c1 -> 0.2c1
314n/a # 1.0preview123 -> 1.0c123
315n/a # PyPI stats: ~21 (0.62%) better
316n/a rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
317n/a
318n/a # Tcl/Tk uses "px" for their post release markers
319n/a rs = re.sub(r"p(\d+)$", r".post\1", rs)
320n/a
321n/a try:
322n/a NormalizedVersion(rs)
323n/a return rs # already rational
324n/a except IrrationalVersionError:
325n/a pass
326n/a return None
327n/a
328n/a
329n/a# A predicate is: "ProjectName (VERSION1, VERSION2, ..)
330n/a_PREDICATE = re.compile(r"(?i)^\s*(\w[\s\w-]*(?:\.\w*)*)(.*)")
331n/a_VERSIONS = re.compile(r"^\s*\((?P<versions>.*)\)\s*$|^\s*"
332n/a "(?P<versions2>.*)\s*$")
333n/a_PLAIN_VERSIONS = re.compile(r"^\s*(.*)\s*$")
334n/a_SPLIT_CMP = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$")
335n/a
336n/a
337n/adef _split_predicate(predicate):
338n/a match = _SPLIT_CMP.match(predicate)
339n/a if match is None:
340n/a # probably no op, we'll use "=="
341n/a comp, version = '==', predicate
342n/a else:
343n/a comp, version = match.groups()
344n/a return comp, NormalizedVersion(version)
345n/a
346n/a
347n/aclass VersionPredicate:
348n/a """Defines a predicate: ProjectName (>ver1,ver2, ..)"""
349n/a
350n/a _operators = {"<": lambda x, y: x < y,
351n/a ">": lambda x, y: x > y,
352n/a "<=": lambda x, y: str(x).startswith(str(y)) or x < y,
353n/a ">=": lambda x, y: str(x).startswith(str(y)) or x > y,
354n/a "==": lambda x, y: str(x).startswith(str(y)),
355n/a "!=": lambda x, y: not str(x).startswith(str(y)),
356n/a }
357n/a
358n/a def __init__(self, predicate):
359n/a self._string = predicate
360n/a predicate = predicate.strip()
361n/a match = _PREDICATE.match(predicate)
362n/a if match is None:
363n/a raise ValueError('Bad predicate "%s"' % predicate)
364n/a
365n/a name, predicates = match.groups()
366n/a self.name = name.strip()
367n/a self.predicates = []
368n/a if predicates is None:
369n/a return
370n/a
371n/a predicates = _VERSIONS.match(predicates.strip())
372n/a if predicates is None:
373n/a return
374n/a
375n/a predicates = predicates.groupdict()
376n/a if predicates['versions'] is not None:
377n/a versions = predicates['versions']
378n/a else:
379n/a versions = predicates.get('versions2')
380n/a
381n/a if versions is not None:
382n/a for version in versions.split(','):
383n/a if version.strip() == '':
384n/a continue
385n/a self.predicates.append(_split_predicate(version))
386n/a
387n/a def match(self, version):
388n/a """Check if the provided version matches the predicates."""
389n/a if isinstance(version, str):
390n/a version = NormalizedVersion(version)
391n/a for operator, predicate in self.predicates:
392n/a if not self._operators[operator](version, predicate):
393n/a return False
394n/a return True
395n/a
396n/a def __repr__(self):
397n/a return self._string
398n/a
399n/a
400n/aclass _Versions(VersionPredicate):
401n/a def __init__(self, predicate):
402n/a predicate = predicate.strip()
403n/a match = _PLAIN_VERSIONS.match(predicate)
404n/a self.name = None
405n/a predicates = match.groups()[0]
406n/a self.predicates = [_split_predicate(pred.strip())
407n/a for pred in predicates.split(',')]
408n/a
409n/a
410n/aclass _Version(VersionPredicate):
411n/a def __init__(self, predicate):
412n/a predicate = predicate.strip()
413n/a match = _PLAIN_VERSIONS.match(predicate)
414n/a self.name = None
415n/a self.predicates = _split_predicate(match.groups()[0])
416n/a
417n/a
418n/adef is_valid_predicate(predicate):
419n/a try:
420n/a VersionPredicate(predicate)
421n/a except (ValueError, IrrationalVersionError):
422n/a return False
423n/a else:
424n/a return True
425n/a
426n/a
427n/adef is_valid_versions(predicate):
428n/a try:
429n/a _Versions(predicate)
430n/a except (ValueError, IrrationalVersionError):
431n/a return False
432n/a else:
433n/a return True
434n/a
435n/a
436n/adef is_valid_version(predicate):
437n/a try:
438n/a _Version(predicate)
439n/a except (ValueError, IrrationalVersionError):
440n/a return False
441n/a else:
442n/a return True
443n/a
444n/a
445n/adef get_version_predicate(requirements):
446n/a """Return a VersionPredicate object, from a string or an already
447n/a existing object.
448n/a """
449n/a if isinstance(requirements, str):
450n/a requirements = VersionPredicate(requirements)
451n/a return requirements