readthedocs.org/readthedocs/projects/version_handling.py

245 lines
7.4 KiB
Python

# -*- coding: utf-8 -*-
"""Project version handling."""
from __future__ import (
absolute_import, division, print_function, unicode_literals)
import unicodedata
from builtins import object, range
from collections import defaultdict
import six
from packaging.version import InvalidVersion, Version
from readthedocs.builds.constants import (
LATEST_VERBOSE_NAME, STABLE_VERBOSE_NAME, TAG)
def get_major(version):
"""
Return the major version.
:param version: version to get the major
:type version: packaging.version.Version
"""
# pylint: disable=protected-access
return version._version.release[0]
def get_minor(version):
"""
Return the minor version.
:param version: version to get the minor
:type version: packaging.version.Version
"""
# pylint: disable=protected-access
try:
return version._version.release[1]
except IndexError:
return 0
class VersionManager(object):
"""Prune list of versions based on version windows."""
def __init__(self):
self._state = defaultdict(lambda: defaultdict(list))
def add(self, version):
self._state[get_major(version)][get_minor(version)].append(version)
def prune_major(self, num_latest):
all_keys = sorted(set(self._state.keys()))
major_keep = []
for __ in range(num_latest):
if all_keys:
major_keep.append(all_keys.pop(-1))
for to_remove in all_keys:
del self._state[to_remove]
def prune_minor(self, num_latest):
for major, minors in list(self._state.items()):
all_keys = sorted(set(minors.keys()))
minor_keep = []
for __ in range(num_latest):
if all_keys:
minor_keep.append(all_keys.pop(-1))
for to_remove in all_keys:
del self._state[major][to_remove]
def prune_point(self, num_latest):
for major, minors in list(self._state.items()):
for minor in list(minors.keys()):
try:
self._state[major][minor] = sorted(
set(self._state[major][minor]))[-num_latest:]
except TypeError:
# Raise these for now.
raise
def get_version_list(self):
versions = []
for major_val in list(self._state.values()):
for version_list in list(major_val.values()):
versions.extend(version_list)
versions = sorted(versions)
return [
version.public for version in versions if not version.is_prerelease
]
def version_windows(versions, major=1, minor=1, point=1):
"""
Return list of versions that have been pruned to version windows.
Uses :py:class:`VersionManager` to prune the list of versions
:param versions: List of version strings
:param major: Major version window
:param minor: Minor version window
:param point: Point version window
"""
# TODO: This needs some documentation on how VersionManager etc works and
# some examples what the expected outcome is.
version_identifiers = []
for version_string in versions:
try:
version_identifiers.append(Version(version_string))
except (InvalidVersion, UnicodeEncodeError):
pass
major_version_window = major
minor_version_window = minor
point_version_window = point
manager = VersionManager()
for v in version_identifiers:
manager.add(v)
manager.prune_major(major_version_window)
manager.prune_minor(minor_version_window)
manager.prune_point(point_version_window)
return manager.get_version_list()
def parse_version_failsafe(version_string):
"""
Parse a version in string form and return Version object.
If there is an error parsing the string, ``None`` is returned.
:param version_string: version as string object (e.g. '3.10.1')
:type version_string: str or unicode
:returns: version object created from a string object
:rtype: packaging.version.Version
"""
if not isinstance(version_string, six.text_type):
uni_version = version_string.decode('utf-8')
else:
uni_version = version_string
try:
normalized_version = unicodedata.normalize('NFKD', uni_version)
ascii_version = normalized_version.encode('ascii', 'ignore')
final_form = ascii_version.decode('ascii')
return Version(final_form)
except (UnicodeError, InvalidVersion):
return None
def comparable_version(version_string):
"""
Can be used as ``key`` argument to ``sorted``.
The ``LATEST`` version shall always beat other versions in comparison.
``STABLE`` should be listed second. If we cannot figure out the version
number then we sort it to the bottom of the list.
:param version_string: version as string object (e.g. '3.10.1' or 'latest')
:type version_string: str or unicode
:returns: a comparable version object (e.g. 'latest' -> Version('99999.0'))
:rtype: packaging.version.Version
"""
comparable = parse_version_failsafe(version_string)
if not comparable:
if version_string == LATEST_VERBOSE_NAME:
comparable = Version('99999.0')
elif version_string == STABLE_VERBOSE_NAME:
comparable = Version('9999.0')
else:
comparable = Version('0.01')
return comparable
def sort_versions(version_list):
"""
Take a list of Version models and return a sorted list.
:param version_list: list of Version models
:type version_list: list(readthedocs.builds.models.Version)
:returns: sorted list in descending order (latest version first) of versions
:rtype: list(tupe(readthedocs.builds.models.Version,
packaging.version.Version))
"""
versions = []
for version_obj in version_list:
version_slug = version_obj.verbose_name
comparable_version = parse_version_failsafe(version_slug)
if comparable_version:
versions.append((version_obj, comparable_version))
return list(
sorted(
versions,
key=lambda version_info: version_info[1],
reverse=True,
))
def highest_version(version_list):
"""
Return the highest version for a given ``version_list``.
:rtype: tupe(readthedocs.builds.models.Version, packaging.version.Version)
"""
versions = sort_versions(version_list)
if versions:
return versions[0]
return (None, None)
def determine_stable_version(version_list):
"""
Determine a stable version for version list.
:param version_list: list of versions
:type version_list: list(readthedocs.builds.models.Version)
:returns: version considered the most recent stable one or ``None`` if there
is no stable version in the list
:rtype: readthedocs.builds.models.Version
"""
versions = sort_versions(version_list)
versions = [(version_obj, comparable)
for version_obj, comparable in versions
if not comparable.is_prerelease]
if versions:
# We take preference for tags over branches. If we don't find any tag,
# we just return the first branch found.
for version_obj, comparable in versions:
if version_obj.type == TAG:
return version_obj
version_obj, comparable = versions[0]
return version_obj
return None