246 lines
8.7 KiB
Python
246 lines
8.7 KiB
Python
"""
|
|
Backport of PEP 562.
|
|
|
|
https://pypi.org/search/?q=pep562
|
|
|
|
Licensed under MIT
|
|
Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
|
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions
|
|
of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
IN THE SOFTWARE.
|
|
"""
|
|
import sys
|
|
from collections import namedtuple
|
|
import re
|
|
|
|
__all__ = ('Pep562',)
|
|
|
|
RE_VER = re.compile(
|
|
r'''(?x)
|
|
(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))?
|
|
(?:(?P<type>a|b|rc)(?P<pre>\d+))?
|
|
(?:\.post(?P<post>\d+))?
|
|
(?:\.dev(?P<dev>\d+))?
|
|
'''
|
|
)
|
|
|
|
REL_MAP = {
|
|
".dev": "",
|
|
".dev-alpha": "a",
|
|
".dev-beta": "b",
|
|
".dev-candidate": "rc",
|
|
"alpha": "a",
|
|
"beta": "b",
|
|
"candidate": "rc",
|
|
"final": ""
|
|
}
|
|
|
|
DEV_STATUS = {
|
|
".dev": "2 - Pre-Alpha",
|
|
".dev-alpha": "2 - Pre-Alpha",
|
|
".dev-beta": "2 - Pre-Alpha",
|
|
".dev-candidate": "2 - Pre-Alpha",
|
|
"alpha": "3 - Alpha",
|
|
"beta": "4 - Beta",
|
|
"candidate": "4 - Beta",
|
|
"final": "5 - Production/Stable"
|
|
}
|
|
|
|
PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
|
|
|
|
|
|
class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
|
|
"""
|
|
Get the version (PEP 440).
|
|
|
|
A biased approach to the PEP 440 semantic version.
|
|
|
|
Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
|
|
(major, minor, micro, release type, pre-release build, post-release build, development release build)
|
|
Release types are named in is such a way they are comparable with ease.
|
|
Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
|
|
development status for setup files.
|
|
|
|
How it works (currently):
|
|
|
|
- You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
|
|
- To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
|
|
The dot is used to ensure all development specifiers are sorted before `alpha`.
|
|
You can specify a `dev` number for development builds, but do not have to as implicit development releases
|
|
are allowed.
|
|
- You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
|
|
allow implicit prereleases.
|
|
- You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
|
|
are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
|
|
noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
|
|
does not allow implicit post releases.
|
|
- It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.
|
|
|
|
Acceptable version releases:
|
|
|
|
```
|
|
Version(1, 0, 0, "final") 1.0
|
|
Version(1, 2, 0, "final") 1.2
|
|
Version(1, 2, 3, "final") 1.2.3
|
|
Version(1, 2, 0, ".dev-alpha", pre=4) 1.2a4
|
|
Version(1, 2, 0, ".dev-beta", pre=4) 1.2b4
|
|
Version(1, 2, 0, ".dev-candidate", pre=4) 1.2rc4
|
|
Version(1, 2, 0, "final", post=1) 1.2.post1
|
|
Version(1, 2, 3, ".dev") 1.2.3.dev0
|
|
Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1
|
|
```
|
|
|
|
"""
|
|
|
|
def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0):
|
|
"""Validate version info."""
|
|
|
|
# Ensure all parts are positive integers.
|
|
for value in (major, minor, micro, pre, post):
|
|
if not (isinstance(value, int) and value >= 0):
|
|
raise ValueError("All version parts except 'release' should be integers.")
|
|
|
|
if release not in REL_MAP:
|
|
raise ValueError("'{}' is not a valid release type.".format(release))
|
|
|
|
# Ensure valid pre-release (we do not allow implicit pre-releases).
|
|
if ".dev-candidate" < release < "final":
|
|
if pre == 0:
|
|
raise ValueError("Implicit pre-releases not allowed.")
|
|
elif dev:
|
|
raise ValueError("Version is not a development release.")
|
|
elif post:
|
|
raise ValueError("Post-releases are not allowed with pre-releases.")
|
|
|
|
# Ensure valid development or development/pre release
|
|
elif release < "alpha":
|
|
if release > ".dev" and pre == 0:
|
|
raise ValueError("Implicit pre-release not allowed.")
|
|
elif post:
|
|
raise ValueError("Post-releases are not allowed with pre-releases.")
|
|
|
|
# Ensure a valid normal release
|
|
else:
|
|
if pre:
|
|
raise ValueError("Version is not a pre-release.")
|
|
elif dev:
|
|
raise ValueError("Version is not a development release.")
|
|
|
|
return super().__new__(cls, major, minor, micro, release, pre, post, dev)
|
|
|
|
def _is_pre(self):
|
|
"""Is prerelease."""
|
|
|
|
return self.pre > 0
|
|
|
|
def _is_dev(self):
|
|
"""Is development."""
|
|
|
|
return bool(self.release < "alpha")
|
|
|
|
def _is_post(self):
|
|
"""Is post."""
|
|
|
|
return self.post > 0
|
|
|
|
def _get_dev_status(self): # pragma: no cover
|
|
"""Get development status string."""
|
|
|
|
return DEV_STATUS[self.release]
|
|
|
|
def _get_canonical(self):
|
|
"""Get the canonical output string."""
|
|
|
|
# Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
|
|
if self.micro == 0:
|
|
ver = "{}.{}".format(self.major, self.minor)
|
|
else:
|
|
ver = "{}.{}.{}".format(self.major, self.minor, self.micro)
|
|
if self._is_pre():
|
|
ver += '{}{}'.format(REL_MAP[self.release], self.pre)
|
|
if self._is_post():
|
|
ver += ".post{}".format(self.post)
|
|
if self._is_dev():
|
|
ver += ".dev{}".format(self.dev)
|
|
|
|
return ver
|
|
|
|
|
|
def parse_version(ver, pre=False):
|
|
"""Parse version into a comparable Version tuple."""
|
|
|
|
m = RE_VER.match(ver)
|
|
|
|
# Handle major, minor, micro
|
|
major = int(m.group('major'))
|
|
minor = int(m.group('minor')) if m.group('minor') else 0
|
|
micro = int(m.group('micro')) if m.group('micro') else 0
|
|
|
|
# Handle pre releases
|
|
if m.group('type'):
|
|
release = PRE_REL_MAP[m.group('type')]
|
|
pre = int(m.group('pre'))
|
|
else:
|
|
release = "final"
|
|
pre = 0
|
|
|
|
# Handle development releases
|
|
dev = m.group('dev') if m.group('dev') else 0
|
|
if m.group('dev'):
|
|
dev = int(m.group('dev'))
|
|
release = '.dev-' + release if pre else '.dev'
|
|
else:
|
|
dev = 0
|
|
|
|
# Handle post
|
|
post = int(m.group('post')) if m.group('post') else 0
|
|
|
|
return Version(major, minor, micro, release, pre, post, dev)
|
|
|
|
|
|
class Pep562:
|
|
"""
|
|
Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
|
|
|
|
Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
|
|
The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
|
|
"""
|
|
|
|
def __init__(self, name):
|
|
"""Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""
|
|
|
|
self._module = sys.modules[name]
|
|
self._get_attr = getattr(self._module, '__getattr__', None)
|
|
self._get_dir = getattr(self._module, '__dir__', None)
|
|
sys.modules[name] = self
|
|
|
|
def __dir__(self):
|
|
"""Return the overridden `dir` if one was provided, else apply `dir` to the module."""
|
|
|
|
return self._get_dir() if self._get_dir else dir(self._module)
|
|
|
|
def __getattr__(self, name):
|
|
"""Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present."""
|
|
|
|
try:
|
|
return getattr(self._module, name)
|
|
except AttributeError:
|
|
if self._get_attr:
|
|
return self._get_attr(name)
|
|
raise
|
|
|
|
|
|
__version_info__ = Version(1, 0, 0, "final")
|
|
__version__ = __version_info__._get_canonical()
|