Initial commit

pull/3/head
Frazer McLean 4 years ago
commit f69c63c526
  1. 11
      .coveragerc
  2. 59
      .gitignore
  3. 22
      README.rst
  4. 48
      setup.py
  5. 5
      src/parver/__init__.py
  6. 4
      src/parver/_about.py
  7. 77
      src/parver/_helpers.py
  8. 223
      src/parver/_parse.py
  9. 53
      src/parver/_segments.py
  10. 368
      src/parver/_version.py
  11. 2
      tests/__init__.py
  12. 144
      tests/strategies.py
  13. 717
      tests/test_packaging.py
  14. 46
      tests/test_parse.py
  15. 112
      tests/test_version.py

@ -0,0 +1,11 @@
[run]
branch = True
source =
parver
tests/
[report]
precision = 1
exclude_lines =
pragma: no cover
pass

59
.gitignore vendored

@ -0,0 +1,59 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.pytest_cache
.hypothesis
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/

@ -0,0 +1,22 @@
.. image:: https://img.shields.io/badge/docs-read%20now-blue.svg
:target: https://parver.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status
.. image:: https://travis-ci.org/RazerM/parver.svg?branch=master
:target: https://travis-ci.org/RazerM/parver
:alt: Automated test status
.. image:: https://codecov.io/gh/RazerM/parver/branch/master/graph/badge.svg
:target: https://codecov.io/gh/RazerM/parver
:alt: Test coverage
.. image:: https://img.shields.io/github/license/RazerM/parver.svg
:target: https://raw.githubusercontent.com/RazerM/parver/master/LICENSE.txt
:alt: MIT License
parver
======
parver allows parsing and manipulation `PEP 440`_ version numbers.
.. _`PEP 440`: https://www.python.org/dev/peps/pep-0440/

@ -0,0 +1,48 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
from io import open
from setuptools import setup, find_packages
about = dict()
# read _version.py as bytes, otherwise exec will complain about
# 'coding: utf-8', which we want there for the normal Python 2 import
with open('src/parver/_about.py', 'rb') as fp:
about_mod = fp.read()
exec(about_mod, about)
LONG_DESC = open('README.rst', encoding='utf-8').read()
setup(
name='parver',
version=about['__version__'],
description='Parse and manipulate version numbers.',
url='https://github.com/RazerM/parver',
long_description=LONG_DESC,
long_description_content_type='text/x-rst',
author='Frazer McLean',
author_email='frazer@frazermclean.co.uk',
license='MIT',
packages=find_packages('src'),
package_dir={'': 'src'},
install_requires=[
'arpeggio',
'attrs >= 17.4',
],
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
keywords='pep440 version parse',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
],
)

@ -0,0 +1,5 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
from ._version import Version
from ._parse import ParseError

@ -0,0 +1,4 @@
# coding: utf-8
# This file is imported from __init__.py and exec'd from setup.py
__version__ = "0.1.0"

@ -0,0 +1,77 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
class UnsetType(object):
def __repr__(self):
return 'UNSET'
UNSET = UnsetType()
del UnsetType
class Infinity(object):
def __repr__(self):
return "Infinity"
def __hash__(self):
return hash(repr(self))
def __lt__(self, other):
return False
def __le__(self, other):
return False
def __eq__(self, other):
return isinstance(other, self.__class__)
def __ne__(self, other):
return not isinstance(other, self.__class__)
def __gt__(self, other):
return True
def __ge__(self, other):
return True
def __neg__(self):
return NegativeInfinity
Infinity = Infinity()
class NegativeInfinity(object):
def __repr__(self):
return "-Infinity"
def __hash__(self):
return hash(repr(self))
def __lt__(self, other):
return True
def __le__(self, other):
return True
def __eq__(self, other):
return isinstance(other, self.__class__)
def __ne__(self, other):
return not isinstance(other, self.__class__)
def __gt__(self, other):
return False
def __ge__(self, other):
return False
def __neg__(self):
return Infinity
NegativeInfinity = NegativeInfinity()

@ -0,0 +1,223 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
import attr
import six
from arpeggio import NoMatch, PTNodeVisitor, visit_parse_tree, Terminal
from arpeggio.cleanpeg import ParserPEG
from ._helpers import UNSET
from . import _segments as segment
canonical = '''
version = epoch? release pre? post? dev? local? EOF
epoch = int "!"
release = int (dot int)*
pre = pre_tag pre_post_num
pre_tag = "a" / "b" / "rc"
post = sep post_tag pre_post_num
pre_post_num = int
post_tag = "post"
dev = sep "dev" int
local = "+" r'([a-zA-Z0-9]+([-_\.][a-zA-Z0-9]+)*)'
sep = dot
dot = "."
int = r'[0-9]+'
alpha = r'[a-zA-Z0-9]'
'''
permissive = '''
version = v? epoch? release pre? (post / post_implicit)? dev? local? EOF
v = "v"
epoch = int "!"
release = int (dot int)*
pre = sep? pre_tag pre_post_num?
pre_tag = "c" / "rc" / "alpha" / "a" / "beta" / "b" / "preview" / "pre"
post = sep? post_tag pre_post_num?
post_implicit = "-" int
post_tag = "post" / "rev" / "r"
pre_post_num = sep? int
dev = sep? "dev" int?
local = "+" r'([a-zA-Z0-9]+([-_\.][a-zA-Z0-9]+)*)'
sep = dot / "-" / "_"
dot = "."
int = r'[0-9]+'
alpha = r'[a-zA-Z0-9]'
'''
_canonical_parser = ParserPEG(canonical, root_rule_name='version', skipws=False)
_permissive_parser = ParserPEG(
permissive, root_rule_name='version', skipws=False, ignore_case=True)
def unwrap_token(value):
if isinstance(value, Token):
return value.value
return value
@attr.s
class Token(object):
value = attr.ib()
def make_token(name):
return type(name, (Token,), dict())
Sep = make_token('Sep')
Tag = make_token('Tag')
VToken = make_token('VToken')
class VersionVisitor(PTNodeVisitor):
def visit_version(self, node, children):
return children
def visit_v(self, node, children):
return segment.V()
def visit_epoch(self, node, children):
return segment.Epoch(children[0])
def visit_release(self, node, children):
return segment.Release(tuple(children))
def visit_pre(self, node, children):
sep1 = UNSET
tag = UNSET
sep2 = UNSET
num = UNSET
for token in children:
if sep1 is UNSET:
if isinstance(token, Sep):
sep1 = token.value
elif isinstance(token, Tag):
sep1 = None
tag = token.value
elif tag is UNSET:
tag = token.value
else:
assert isinstance(token, tuple)
assert len(token) == 2
sep2 = token[0].value
num = token[1]
if sep2 is UNSET:
sep2 = None
num = None
assert sep1 is not UNSET
assert tag is not UNSET
assert sep2 is not UNSET
assert num is not UNSET
return segment.Pre(sep1=sep1, tag=tag, sep2=sep2, value=num)
def visit_pre_post_num(self, node, children):
# when "pre_post_num = int", visit_int isn't called for some reason
# I don't understand. Let's call int() manually
if isinstance(node, Terminal):
return Sep(None), int(node.value)
if len(children) == 1:
return Sep(None), children[0]
else:
return tuple(children[:2])
def visit_pre_tag(self, node, children):
return Tag(node.value)
def visit_post(self, node, children):
sep1 = UNSET
tag = UNSET
sep2 = UNSET
num = UNSET
for token in children:
if sep1 is UNSET:
if isinstance(token, Sep):
sep1 = token.value
elif isinstance(token, Tag):
sep1 = None
tag = token.value
elif tag is UNSET:
tag = token.value
else:
assert isinstance(token, tuple)
assert len(token) == 2
sep2 = token[0].value
num = token[1]
if sep2 is UNSET:
sep2 = None
num = None
assert sep1 is not UNSET
assert tag is not UNSET
assert sep2 is not UNSET
assert num is not UNSET
return segment.Post(sep1=sep1, tag=tag, sep2=sep2, value=num)
def visit_post_tag(self, node, children):
return Tag(node.value)
def visit_post_implicit(self, node, children):
return segment.Post(sep1=UNSET, tag=None, sep2=UNSET, value=children[0])
def visit_dev(self, node, children):
num = None
sep = UNSET
for token in children:
if sep is UNSET:
if isinstance(token, Sep):
sep = token.value
else:
num = token
else:
num = token
if sep is UNSET:
sep = None
return segment.Dev(value=num, sep=sep)
def visit_local(self, node, children):
return segment.Local(''.join(children))
def visit_int(self, node, children):
return int(node.value)
def visit_sep(self, node, children):
return Sep(node.value)
def noop(self, node, children):
pass
visit_xsep = noop
visit_xdot = noop
class ParseError(Exception):
pass
def parse_canonical(version):
try:
tree = _canonical_parser.parse(version.strip())
except NoMatch as exc:
six.raise_from(ParseError(str(exc)), None)
return visit_parse_tree(tree, VersionVisitor())
def parse_permissive(version):
try:
tree = _permissive_parser.parse(version.strip().lower())
except NoMatch as exc:
six.raise_from(ParseError(str(exc)), None)
return visit_parse_tree(tree, VersionVisitor())

@ -0,0 +1,53 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
import attr
@attr.s(slots=True)
class Segment(object):
pass
@attr.s(slots=True)
class V(Segment):
pass
@attr.s(slots=True)
class ValueSegment(Segment):
value = attr.ib()
@attr.s(slots=True)
class Epoch(ValueSegment):
pass
@attr.s(slots=True)
class Release(ValueSegment):
pass
@attr.s(slots=True)
class Pre(ValueSegment):
sep1 = attr.ib()
tag = attr.ib()
sep2 = attr.ib()
@attr.s(slots=True)
class Post(ValueSegment):
sep1 = attr.ib()
tag = attr.ib()
sep2 = attr.ib()
@attr.s(slots=True)
class Dev(ValueSegment):
sep = attr.ib()
@attr.s(slots=True)
class Local(ValueSegment):
pass

@ -0,0 +1,368 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
import itertools
import re
import six
from functools import partial
import attr
from attr.validators import in_, instance_of, optional
from ._helpers import UNSET, Infinity, NegativeInfinity
from ._parse import parse_canonical, parse_permissive
from . import _segments as segment
def force_tuple(n):
if not isinstance(n, tuple):
return n,
return n
POST_TAGS = {'post', 'rev', 'r'}
SEPS = {'.', '-', '_'}
PRE_TAGS = {'c', 'rc', 'alpha', 'a', 'beta', 'b', 'preview', 'pre'}
validate_post_tag = optional(in_(POST_TAGS | {UNSET}))
validate_pre_tag = optional(in_(PRE_TAGS))
validate_sep = optional(in_(SEPS))
validate_sep_or_unset = optional(in_(SEPS | {UNSET}))
is_bool = instance_of(bool)
is_int = instance_of(int)
is_str = instance_of(six.string_types)
def unset_or(validator):
def validate(inst, attr, value):
if value is UNSET:
return
validator(inst, attr, value)
return validate
@attr.s(frozen=True, repr=False, cmp=False)
class Version(object):
release = attr.ib(converter=force_tuple)
v = attr.ib(default=False, validator=is_bool)
epoch = attr.ib(default=None, validator=optional(is_int))
pre_tag = attr.ib(default=None, validator=validate_pre_tag)
pre = attr.ib(default=None, validator=optional(is_int))
post = attr.ib(default=UNSET, validator=unset_or(optional(is_int)))
dev = attr.ib(default=UNSET, validator=unset_or(optional(is_int)))
local = attr.ib(default=None, validator=optional(is_str))
pre_sep1 = attr.ib(default=None, validator=validate_sep)
pre_sep2 = attr.ib(default=None, validator=validate_sep)
post_sep1 = attr.ib(default=UNSET, validator=validate_sep_or_unset)
post_sep2 = attr.ib(default=UNSET, validator=validate_sep_or_unset)
dev_sep = attr.ib(default='.', validator=validate_sep)
post_tag = attr.ib(default=UNSET, validator=validate_post_tag)
epoch_implicit = attr.ib(default=False, init=False)
pre_implicit = attr.ib(default=False, init=False)
post_implicit = attr.ib(default=False, init=False)
dev_implicit = attr.ib(default=False, init=False)
_key = attr.ib(init=False)
def __attrs_post_init__(self):
set = partial(object.__setattr__, self)
if self.epoch is None:
set('epoch', 0)
set('epoch_implicit', True)
if self.pre_tag is not None and self.pre is None:
set('pre', 0)
set('pre_implicit', True)
if self.pre is not None and self.pre_tag is None:
raise ValueError('Must set pre_tag if pre is given.')
if (self.pre_tag is None and
(self.pre_sep1 is not None or self.pre_sep2 is not None)):
raise ValueError('Cannot set pre_sep1 or pre_sep2 without pre_tag.')
if self.post_tag is None:
if self.post is UNSET:
raise ValueError(
"Implicit post releases (post_tag=None) require a numerical "
"value for 'post' argument.")
if self.post_sep1 is not UNSET or self.post_sep2 is not UNSET:
raise ValueError(
'post_sep1 and post_sep2 cannot be set for implicit post '
'releases (post_tag=None)')
if self.post_sep1 is UNSET:
set('post_sep1', '.')
if self.post_sep2 is UNSET:
set('post_sep2', None)
if self.post is not UNSET:
if self.post_tag is UNSET:
set('post_tag', 'post')
if self.post is None:
set('post_implicit', True)
set('post', 0)
if self.post_tag is not UNSET and self.post is UNSET:
set('post_implicit', True)
set('post', 0)
if self.dev is None:
set('dev_implicit', True)
set('dev', 0)
if self.post is UNSET:
set('post', None)
if self.post_tag is UNSET:
set('post_tag', None)
if self.dev is UNSET:
set('dev', None)
assert self.post_sep1 is not UNSET
assert self.post_sep2 is not UNSET
set('_key', _cmpkey(
self.epoch,
self.release,
_normalize_pre_tag(self.pre_tag),
self.pre,
self.post,
self.dev,
self.local,
))
@classmethod
def parse(cls, version, strict=False):
parse = parse_canonical if strict else parse_permissive
segments = parse(version)
kwargs = dict()
for s in segments:
if isinstance(s, segment.Epoch):
kwargs['epoch'] = s.value
elif isinstance(s, segment.Release):
kwargs['release'] = s.value
elif isinstance(s, segment.Pre):
kwargs['pre'] = s.value
kwargs['pre_tag'] = s.tag
kwargs['pre_sep1'] = s.sep1
kwargs['pre_sep2'] = s.sep2
elif isinstance(s, segment.Post):
kwargs['post'] = s.value
kwargs['post_tag'] = s.tag
kwargs['post_sep1'] = s.sep1
kwargs['post_sep2'] = s.sep2
elif isinstance(s, segment.Dev):
kwargs['dev'] = s.value
kwargs['dev_sep'] = s.sep
elif isinstance(s, segment.Local):
kwargs['local'] = s.value
elif isinstance(s, segment.V):
kwargs['v'] = True
else:
raise TypeError('Unexpected segment: {}'.format(segment))
return cls(**kwargs)
def normalize(self):
return Version(
release=self.release,
epoch=None if self.epoch == 0 else self.epoch,
pre_tag=_normalize_pre_tag(self.pre_tag),
pre=self.pre,
post=UNSET if self.post is None else self.post,
dev=UNSET if self.dev is None else self.dev,
local=self.local
)
def __str__(self):
parts = []
if self.v:
parts.append('v')
if not self.epoch_implicit:
parts.append('{}!'.format(self.epoch))
parts.append('.'.join(str(x) for x in self.release))
if self.pre_tag is not None:
if self.pre_sep1:
parts.append(self.pre_sep1)
parts.append(self.pre_tag)
if self.pre_sep2:
parts.append(self.pre_sep2)
if not self.pre_implicit:
parts.append(str(self.pre))
if self.post_tag is None and self.post is not None:
parts.append('-{}'.format(self.post))
elif self.post_tag is not None:
if self.post_sep1:
parts.append(self.post_sep1)
parts.append(self.post_tag)
if self.post_sep2:
parts.append(self.post_sep2)
if not self.post_implicit:
parts.append(str(self.post))
if self.dev is not None:
if self.dev_sep is not None:
parts.append(self.dev_sep)
parts.append('dev')
if not self.dev_implicit:
parts.append(str(self.dev))
if self.local is not None:
parts.append('+{}'.format(self.local))
return ''.join(parts)
def __repr__(self):
return '<{} {!r}>'.format(self.__class__.__name__, str(self))
def __hash__(self):
return hash(self._key)
def __lt__(self, other):
return self._compare(other, lambda s, o: s < o)
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
def __ge__(self, other):
return self._compare(other, lambda s, o: s >= o)
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
def __ne__(self, other):
return self._compare(other, lambda s, o: s != o)
def _compare(self, other, method):
if not isinstance(other, Version):
return NotImplemented
return method(self._key, other._key)
@property
def public(self):
return str(self).split('+', 1)[0]
@property
def base_version(self):
parts = []
# Epoch
if self.epoch != 0:
parts.append('{0}!'.format(self.epoch))
# Release segment
parts.append('.'.join(str(x) for x in self.release))
return ''.join(parts)
@property
def is_prerelease(self):
return self.dev is not None or self.pre is not None
@property
def is_postrelease(self):
return self.post is not None
@property
def is_devrelease(self):
return self.dev is not None
def _normalize_pre_tag(pre_tag):
if pre_tag is None:
return None
if pre_tag == 'alpha':
pre_tag = 'a'
elif pre_tag == 'beta':
pre_tag = 'b'
elif pre_tag in {'c', 'pre', 'preview'}:
pre_tag = 'rc'
return pre_tag
def _cmpkey(epoch, release, pre_tag, pre_num, post, dev, local):
# When we compare a release version, we want to compare it with all of the
# trailing zeros removed. So we'll use a reverse the list, drop all the now
# leading zeros until we come to something non zero, then take the rest
# re-reverse it back into the correct order and make it a tuple and use
# that for our sorting key.
release = tuple(
reversed(list(
itertools.dropwhile(
lambda x: x == 0,
reversed(release),
)
))
)
pre = pre_tag, pre_num
# We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
# We'll do this by abusing the pre segment, but we _only_ want to do this
# if there is not a pre or a post segment. If we have one of those then
# the normal sorting rules will handle this case correctly.
if pre_num is None and post is None and dev is not None:
pre = -Infinity
# Versions without a pre-release (except as noted above) should sort after
# those with one.
elif pre_num is None:
pre = Infinity
# Versions without a post segment should sort before those with one.
if post is None:
post = -Infinity
# Versions without a development segment should sort after those with one.
if dev is None:
dev = Infinity
if local is None:
# Versions without a local segment should sort before those with one.
local = -Infinity
else:
# Versions with a local segment need that segment parsed to implement
# the sorting rules in PEP440.
# - Alpha numeric segments sort before numeric segments
# - Alpha numeric segments sort lexicographically
# - Numeric segments sort numerically
# - Shorter versions sort before longer versions when the prefixes
# match exactly
local = tuple(
(i, "") if isinstance(i, int) else (-Infinity, i)
for i in _parse_local_version(local)
)
return epoch, release, pre, post, dev, local
_local_version_separators = re.compile(r"[._-]")
def _parse_local_version(local):
"""
Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
"""
if local is not None:
return tuple(
part.lower() if not part.isdigit() else int(part)
for part in _local_version_separators.split(local)
)

@ -0,0 +1,2 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function

@ -0,0 +1,144 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function
import string
from hypothesis.strategies import (
composite, integers, just, lists, one_of, sampled_from, text)
num_int = integers(min_value=0)
num_str = num_int.map(str)
def epoch():
epoch = num_str.map(lambda s: s + '!')
return one_of(just(''), epoch)
@composite
def release(draw):
a = draw(num_str)
parts = [a] + draw(lists(num_str.map(lambda s: '.' + s)))
return ''.join(parts)
def separator(strict=False, optional=False):
sep = ['.']
if optional:
sep.append('')
if not strict:
sep.extend(['-', '_'])
return sampled_from(sep)
@composite
def pre(draw, strict=False):
words = ['a', 'b', 'rc']
if not strict:
words.extend(['c', 'alpha', 'beta', 'pre', 'preview'])
sep1 = draw(separator(strict=strict, optional=True))
if strict:
sep1 = ''
word = draw(sampled_from(words))
if strict:
sep2 = ''
else:
sep2 = draw(separator(strict=strict, optional=True))
n = draw(num_str)
if strict:
num_part = draw(just(sep2 + n))
else:
num_part = draw(one_of(just(''), just(sep2 + n)))
return sep1 + word + num_part
@composite
def post(draw, strict=False):
words = ['post']
if not strict:
words.extend(['r', 'rev'])
sep1 = draw(separator(strict=strict, optional=not strict))
word = draw(sampled_from(words))
sep2 = draw(separator(strict=strict, optional=True))
if strict:
sep2 = ''
n = draw(num_str)
if strict:
num_part = draw(just(sep2 + n))
else:
num_part = draw(one_of(just(''), just(sep2 + n)))
post = sep1 + word + num_part
if strict:
return post
post = just(post)
post_implicit = num_str.map(lambda s: '-' + s)
return draw(one_of(post_implicit, post))
@composite
def dev(draw, strict=False):
sep = draw(separator(strict=strict, optional=not strict))
if strict:
num_part = draw(num_str)
else:
num_part = draw(one_of(just(''), num_str))
return sep + 'dev' + num_part
@composite
def local_segment(draw, strict=False):
if strict:
sep = just('.')
else:
sep = sampled_from('-_.')
return draw(sep) + draw(text(string.ascii_lowercase + string.digits, min_size=1))
@composite
def local(draw, strict=False):
start = draw(text(string.ascii_lowercase + string.digits, min_size=1))
end = ''.join(draw(lists(local_segment(strict=strict))))
return draw(one_of(just(''), just('+' + start + end)))
whitespace = sampled_from(['', '\t', '\n', '\r', '\f', '\v'])
def vchar(strict=False):
if strict:
return just('')
return sampled_from(['', 'v'])
@composite
def version_string(draw, strict=False):
return (
draw(vchar(strict=strict)) +
draw(epoch()) +
draw(release()) +
draw(pre(strict=strict)) +
draw(post(strict=strict)) +
draw(dev(strict=strict)) +
draw(local(strict=strict))
)

@ -0,0 +1,717 @@
# coding: utf-8
"""Tests borrowed from https://github.com/pypa/packaging"""
from __future__ import absolute_import, division, print_function
import itertools
import operator
import pretend
import pytest
from parver import ParseError, Version
# This list must be in the correct sorting order
VERSIONS = [
# Implicit epoch of 0
"1.0.dev456", "1.0a1", "1.0a2.dev456", "1.0a12.dev456", "1.0a12",
"1.0b1.dev456", "1.0b2", "1.0b2.post345.dev456", "1.0b2.post345",
"1.0b2-346", "1.0c1.dev456", "1.0c1", "1.0rc2", "1.0c3", "1.0",
"1.0.post456.dev34", "1.0.post456", "1.1.dev1", "1.2+123abc",
"1.2+123abc456", "1.2+abc", "1.2+abc123", "1.2+abc123def", "1.2+1234.abc",
"1.2+123456", "1.2.r32+123456", "1.2.rev33+123456",
# Explicit epoch of 1
"1!1.0.dev456", "1!1.0a1", "1!1.0a2.dev456", "1!1.0a12.dev456", "1!1.0a12",
"1!1.0b1.dev456", "1!1.0b2", "1!1.0b2.post345.dev456", "1!1.0b2.post345",
"1!1.0b2-346", "1!1.0c1.dev456", "1!1.0c1", "1!1.0rc2", "1!1.0c3", "1!1.0",
"1!1.0.post456.dev34", "1!1.0.post456", "1!1.1.dev1", "1!1.2+123abc",
"1!1.2+123abc456", "1!1.2+abc", "1!1.2+abc123", "1!1.2+abc123def",
"1!1.2+1234.abc", "1!1.2+123456", "1!1.2.r32+123456", "1!1.2.rev33+123456",
]
class TestVersion:
@pytest.mark.parametrize("version", VERSIONS)
def test_valid_versions(self, version):
Version.parse(version)
@pytest.mark.parametrize(
"version",
[
# Non sensical versions should be invalid
"french toast",
# Versions with invalid local versions
"1.0+a+",
"1.0++",
"1.0+_foobar",
"1.0+foo&asd",
"1.0+1+1",
]
)
def test_invalid_versions(self, version):
with pytest.raises(ParseError):
Version.parse(version)
@pytest.mark.parametrize(
("version", "normalized"),
[
# Various development release incarnations
("1.0dev", "1.0.dev0"),
("1.0.dev", "1.0.dev0"),
("1.0dev1", "1.0.dev1"),
("1.0dev", "1.0.dev0"),
("1.0-dev", "1.0.dev0"),
("1.0-dev1", "1.0.dev1"),
("1.0DEV", "1.0.dev0"),
("1.0.DEV", "1.0.dev0"),
("1.0DEV1", "1.0.dev1"),
("1.0DEV", "1.0.dev0"),
("1.0.DEV1", "1.0.dev1"),
("1.0-DEV", "1.0.dev0"),
("1.0-DEV1", "1.0.dev1"),
# Various alpha incarnations
("1.0a", "1.0a0"),
("1.0.a", "1.0a0"),
("1.0.a1", "1.0a1"),
("1.0-a", "1.0a0"),
("1.0-a1", "1.0a1"),
("1.0alpha", "1.0a0"),
("1.0.alpha", "1.0a0"),
("1.0.alpha1", "1.0a1"),
("1.0-alpha", "1.0a0"),
("1.0-alpha1", "1.0a1"),
("1.0A", "1.0a0"),
("1.0.A", "1.0a0"),
("1.0.A1", "1.0a1"),
("1.0-A", "1.0a0"),
("1.0-A1", "1.0a1"),
("1.0ALPHA", "1.0a0"),
("1.0.ALPHA", "1.0a0"),
("1.0.ALPHA1", "1.0a1"),
("1.0-ALPHA", "1.0a0"),
("1.0-ALPHA1", "1.0a1"),
# Various beta incarnations
("1.0b", "1.0b0"),
("1.0.b", "1.0b0"),
("1.0.b1", "1.0b1"),
("1.0-b", "1.0b0"),
("1.0-b1", "1.0b1"),
("1.0beta", "1.0b0"),
("1.0.beta", "1.0b0"),
("1.0.beta1", "1.0b1"),
("1.0-beta", "1.0b0"),
("1.0-beta1", "1.0b1"),
("1.0B", "1.0b0"),
("1.0.B", "1.0b0"),
("1.0.B1", "1.0b1"),
("1.0-B", "1.0b0"),
("1.0-B1", "1.0b1"),
("1.0BETA", "1.0b0"),
("1.0.BETA", "1.0b0"),
("1.0.BETA1", "1.0b1"),
("1.0-BETA", "1.0b0"),
("1.0-BETA1", "1.0b1"),
# Various release candidate incarnations
("1.0c", "1.0rc0"),
("1.0.c", "1.0rc0"),
("1.0.c1", "1.0rc1"),
("1.0-c", "1.0rc0"),
("1.0-c1", "1.0rc1"),
("1.0rc", "1.0rc0"),
("1.0.rc", "1.0rc0"),
("1.0.rc1", "1.0rc1"),
("1.0-rc", "1.0rc0"),
("1.0-rc1", "1.0rc1"),
("1.0C", "1.0rc0"),
("1.0.C", "1.0rc0"),
("1.0.C1", "1.0rc1"),
("1.0-C", "1.0rc0"),
("1.0-C1", "1.0rc1"),
("1.0RC", "1.0rc0"),
("1.0.RC", "1.0rc0"),
("1.0.RC1", "1.0rc1"),
("1.0-RC", "1.0rc0"),
("1.0-RC1", "1.0rc1"),
# Various post release incarnations
("1.0post", "1.0.post0"),
("1.0.post", "1.0.post0"),
("1.0post1", "1.0.post1"),
("1.0post", "1.0.post0"),
("1.0-post", "1.0.post0"),
("1.0-post1", "1.0.post1"),
("1.0POST", "1.0.post0"),
("1.0.POST", "1.0.post0"),
("1.0POST1", "1.0.post1"),
("1.0POST", "1.0.post0"),
("1.0r", "1.0.post0"),
("1.0rev", "1.0.post0"),
("1.0.POST1", "1.0.post1"),
("1.0.r1", "1.0.post1"),
("1.0.rev1", "1.0.post1"),
("1.0-POST", "1.0.post0"),
("1.0-POST1", "1.0.post1"),
("1.0-5", "1.0.post5"),
("1.0-r5", "1.0.post5"),
("1.0-rev5", "1.0.post5"),
# Local version case insensitivity
("1.0+AbC", "1.0+abc"),
# Integer Normalization
("1.01", "1.1"),
("1.0a05", "1.0a5"),
("1.0b07", "1.0b7"),
("1.0c056", "1.0rc56"),
("1.0rc09", "1.0rc9"),
("1.0.post000", "1.0.post0"),
("1.1.dev09000", "1.1.dev9000"),
("00!1.2", "1.2"),
("0100!0.0", "100!0.0"),
# Various other normalizations
("v1.0", "1.0"),
(" v1.0\t\n", "1.0"),
],
)
def test_normalized_versions(self, version, normalized):
assert str(Version.parse(version).normalize()) == normalized
@pytest.mark.parametrize(
("version", "expected"),
[
("1.0.dev456", "1.0.dev456"),
("1.0a1", "1.0a1"),
("1.0a2.dev456", "1.0a2.dev456"),
("1.0a12.dev456", "1.0a12.dev456"),
("1.0a12", "1.0a12"),
("1.0b1.dev456", "1.0b1.dev456"),
("1.0b2", "1.0b2"),
("1.0b2.post345.dev456", "1.0b2.post345.dev456"),
("1.0b2.post345", "1.0b2.post345"),
("1.0rc1.dev456", "1.0rc1.dev456"),
("1.0rc1", "1.0rc1"),
("1.0", "1.0"),
("1.0.post456.dev34", "1.0.post456.dev34"),
("1.0.post456", "1.0.post456"),
("1.0.1", "1.0.1"),
("0!1.0.2", "1.0.2"),
("1.0.3+7", "1.0.3+7"),
("0!1.0.4+8.0", "1.0.4+8.0"),
("1.0.5+9.5", "1.0.5+9.5"),
("1.2+1234.abc", "1.2+1234.abc"),
("1.2+123456", "1.2+123456"),
("1.2+123abc", "1.2+123abc"),
("1.2+123abc456", "1.2+123abc456"),
("1.2+abc", "1.2+abc"),
("1.2+abc123", "1.2+abc123"),
("1.2+abc123def", "1.2+abc123def"),
("1.1.dev1", "1.1.dev1"),
("7!1.0.dev456", "7!1.0.dev456"),
("7!1.0a1", "7!1.0a1"),
("7!1.0a2.dev456", "7!1.0a2.dev456"),
("7!1.0a12.dev456", "7!1.0a12.dev456"),
("7!1.0a12", "7!1.0a12"),
("7!1.0b1.dev456", "7!1.0b1.dev456"),
("7!1.0b2", "7!1.0b2"),
("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"),
("7!1.0b2.post345", "7!1.0b2.post345"),
("7!1.0rc1.dev456", "7!1.0rc1.dev456"),
("7!1.0rc1", "7!1.0rc1"),
("7!1.0", "7!1.0"),
("7!1.0.post456.dev34", "7!1.0.post456.dev34"),
("7!1.0.post456", "7!1.0.post456"),
("7!1.0.1", "7!1.0.1"),
("7!1.0.2", "7!1.0.2"),
("7!1.0.3+7", "7!1.0.3+7"),
("7!1.0.4+8.0", "7!1.0.4+8.0"),
("7!1.0.5+9.5", "7!1.0.5+9.5"),
("7!1.1.dev1", "7!1.1.dev1"),
],
)
def test_version_str_repr(self, version, expected):
v = Version.parse(version).normalize()
assert str(v) == expected
assert (repr(v) == "<Version {0}>".format(repr(expected)))
def test_version_rc_and_c_equals(self):
assert Version.parse("1.0rc1") == Version.parse("1.0c1")
@pytest.mark.parametrize("version", VERSIONS)
def test_version_hash(self, version):
assert hash(Version.parse(version)) == hash(Version.parse(version))
@pytest.mark.parametrize(
("version", "public"),
[
("1.0", "1.0"),
("1.0.dev0", "1.0.dev0"),
("1.0.dev6", "1.0.dev6"),
("1.0a1", "1.0a1"),
("1.0a1.post5", "1.0a1.post5"),
("1.0a1.post5.dev6", "1.0a1.post5.dev6"),
("1.0rc4", "1.0rc4"),
("1.0.post5", "1.0.post5"),
("1!1.0", "1!1.0"),
("1!1.0.dev6", "1!1.0.dev6"),
("1!1.0a1", "1!1.0a1"),
("1!1.0a1.post5", "1!1.0a1.post5"),
("1!1.0a1.post5.dev6", "1!1.0a1.post5.dev6"),
("1!1.0rc4", "1!1.0rc4"),
("1!1.0.post5", "1!1.0.post5"),
("1.0+deadbeef", "1.0"),
("1.0.dev6+deadbeef", "1.0.dev6"),
("1.0a1+deadbeef", "1.0a1"),
("1.0a1.post5+deadbeef", "1.0a1.post5"),
("1.0a1.post5.dev6+deadbeef", "1.0a1.post5.dev6"),
("1.0rc4+deadbeef", "1.0rc4"),
("1.0.post5+deadbeef", "1.0.post5"),
("1!1.0+deadbeef", "1!1.0"),
("1!1.0.dev6+deadbeef", "1!1.0.dev6"),
("1!1.0a1+deadbeef", "1!1.0a1"),
("1!1.0a1.post5+deadbeef", "1!1.0a1.post5"),
("1!1.0a1.post5.dev6+deadbeef", "1!1.0a1.post5.dev6"),
("1!1.0rc4+deadbeef", "1!1.0rc4"),
("1!1.0.post5+deadbeef", "1!1.0.post5"),
],
)
def test_version_public(self, version, public):
assert Version.parse(version).public == public
@pytest.mark.parametrize(
("version", "base_version"),
[
("1.0", "1.0"),
("1.0.dev0", "1.0"),
("1.0.dev6", "1.0"),
("1.0a1", "1.0"),
("1.0a1.post5", "1.0"),
("1.0a1.post5.dev6", "1.0"),
("1.0rc4", "1.0"),
("1.0.post5", "1.0"),
("1!1.0", "1!1.0"),
("1!1.0.dev6", "1!1.0"),
("1!1.0a1", "1!1.0"),
("1!1.0a1.post5", "1!1.0"),
("1!1.0a1.post5.dev6", "1!1.0"),
("1!1.0rc4", "1!1.0"),
("1!1.0.post5", "1!1.0"),
("1.0+deadbeef", "1.0"),
("1.0.dev6+deadbeef", "1.0"),
("1.0a1+deadbeef", "1.0"),
("1.0a1.post5+deadbeef", "1.0"),
("1.0a1.post5.dev6+deadbeef", "1.0"),
("1.0rc4+deadbeef", "1.0"),
("1.0.post5+deadbeef", "1.0"),
("1!1.0+deadbeef", "1!1.0"),
("1!1.0.dev6+deadbeef", "1!1.0"),
("1!1.0a1+deadbeef", "1!1.0"),
("1!1.0a1.post5+deadbeef", "1!1.0"),
("1!1.0a1.post5.dev6+deadbeef", "1!1.0"),
("1!1.0rc4+deadbeef", "1!1.0"),
("1!1.0.post5+deadbeef", "1!1.0"),
],
)
def test_version_base_version(self, version, base_version):
assert Version.parse(version).base_version == base_version
@pytest.mark.parametrize(
("version", "epoch"),
[
("1.0", 0),
("1.0.dev0", 0),
("1.0.dev6", 0),