Skip to content

Commit b8e6086

Browse files
committed
Merge pull request #1384 from gregmuellegger/issue/1017
Using packaging.version for version number parsing, beeing PEP-440 compliant
2 parents afe4347 + b95618f commit b8e6086

File tree

14 files changed

+519
-405
lines changed

14 files changed

+519
-405
lines changed

docs/versions.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,24 @@ You should push a **tag** for each version of your project.
2222
These tags should be numbered in a way that is consistent with `semantic versioning <http://semver.org/>`_.
2323
This will map to your ``stable`` branch by default.
2424

25+
.. note::
26+
We in fact are parsing your tag names against the rules given by
27+
`PEP 440`_. This spec allows "normal" version numbers like ``1.4.2`` as
28+
well as pre-releases. A alpha version or a release candidate are examples
29+
of pre-releases and they look like this: ``2.0a1``.
30+
31+
We only consider non pre-releases for the ``stable`` version of your
32+
documentation.
33+
2534
If you have documentation changes on a **long-lived branch**,
2635
you can build those too.
2736
This will allow you to see how the new docs will be built in this branch of the code.
2837
Generally you won't have more than 1 active branch over a long period of time.
2938
The main exception here would be **release branches**,
3039
which are branches that are maintained over time for a specific release number.
3140

41+
.. _PEP 440: https://www.python.org/dev/peps/pep-0440/
42+
3243
Redirects on root URLs
3344
----------------------
3445

readthedocs/api/base.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from builds.models import Build, Version
1717
from core.utils import trigger_build
1818
from projects.models import Project, ImportedFile
19-
from projects.utils import highest_version, mkversion
19+
from projects.version_handling import highest_version
20+
from projects.version_handling import parse_version_failsafe
2021
from djangome import views as djangome
2122

2223
from .utils import SearchMixin, PostAuthentication
@@ -141,25 +142,28 @@ def get_object_list(self, request):
141142

142143
def version_compare(self, request, **kwargs):
143144
project = get_object_or_404(Project, slug=kwargs['project_slug'])
144-
highest = highest_version(project.versions.filter(active=True))
145+
highest_version_obj, highest_version_comparable = highest_version(
146+
project.versions.filter(active=True))
145147
base = kwargs.get('base', None)
146148
ret_val = {
147-
'project': highest[0],
148-
'version': highest[1],
149+
'project': highest_version_obj,
150+
'version': highest_version_comparable,
149151
'is_highest': True,
150152
}
151-
if highest[0]:
152-
ret_val['url'] = highest[0].get_absolute_url()
153-
ret_val['slug'] = highest[0].slug,
153+
if highest_version_obj:
154+
ret_val['url'] = highest_version_obj.get_absolute_url()
155+
ret_val['slug'] = highest_version_obj.slug,
154156
if base and base != LATEST:
155157
try:
156-
ver_obj = project.versions.get(slug=base)
157-
base_ver = mkversion(ver_obj)
158-
if base_ver:
158+
base_version_obj = project.versions.get(slug=base)
159+
base_version_comparable = parse_version_failsafe(
160+
base_version_obj.verbose_name)
161+
if base_version_comparable:
159162
# This is only place where is_highest can get set. All
160163
# error cases will be set to True, for non- standard
161164
# versions.
162-
ret_val['is_highest'] = base_ver >= highest[1]
165+
ret_val['is_highest'] = (
166+
base_version_comparable >= highest_version_comparable)
163167
else:
164168
ret_val['is_highest'] = True
165169
except (Version.DoesNotExist, TypeError):

readthedocs/betterversion/__init__.py

Whitespace-only changes.

readthedocs/betterversion/better.py

Lines changed: 0 additions & 85 deletions
This file was deleted.

readthedocs/projects/models.py

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import os
44
from urlparse import urlparse
55

6-
from distlib.version import UnsupportedVersionError
76
from django.conf import settings
87
from django.contrib.auth.models import User
98
from django.core.urlresolvers import reverse
@@ -13,16 +12,17 @@
1312

1413
from guardian.shortcuts import assign
1514

16-
from betterversion.better import version_windows, VersionIdentifier
1715
from builds.constants import LATEST
1816
from builds.constants import LATEST_VERBOSE_NAME
17+
from builds.constants import STABLE
1918
from oauth import utils as oauth_utils
2019
from privacy.loader import RelatedProjectManager, ProjectManager
2120
from projects import constants
2221
from projects.exceptions import ProjectImportError
2322
from projects.templatetags.projects_tags import sort_version_aware
24-
from projects.utils import (highest_version as _highest, make_api_version,
25-
symlink, update_static_metadata)
23+
from projects.utils import make_api_version, symlink, update_static_metadata
24+
from projects.version_handling import determine_stable_version
25+
from projects.version_handling import version_windows
2626
from taggit.managers import TaggableManager
2727
from tastyapi.slum import api
2828

@@ -248,7 +248,7 @@ def subdomain(self):
248248
return "%s.%s" % (subdomain_slug, prod_domain)
249249

250250
def sync_supported_versions(self):
251-
supported = self.supported_versions(flat=True)
251+
supported = self.supported_versions()
252252
if supported:
253253
self.versions.filter(
254254
verbose_name__in=supported).update(supported=True)
@@ -590,10 +590,6 @@ def conf_dir(self, version=LATEST):
590590
if conf_file:
591591
return conf_file.replace('/conf.py', '')
592592

593-
@property
594-
def highest_version(self):
595-
return _highest(self.api_versions())
596-
597593
@property
598594
def is_imported(self):
599595
return bool(self.repo)
@@ -697,37 +693,59 @@ def all_active_versions(self):
697693
"""
698694
return self.versions.filter(active=True)
699695

700-
def supported_versions(self, flat=True):
696+
def supported_versions(self):
701697
"""
702698
Get the list of supported versions.
703699
Returns a list of version strings.
704700
"""
705701
if not self.num_major or not self.num_minor or not self.num_point:
706-
return None
707-
version_identifiers = []
708-
for version in self.versions.all():
709-
try:
710-
version_identifiers.append(VersionIdentifier(version.verbose_name))
711-
except UnsupportedVersionError:
712-
# Probably a branch
713-
pass
714-
active_versions = version_windows(
702+
return []
703+
version_identifiers = self.versions.values_list('verbose_name', flat=True)
704+
return version_windows(
715705
version_identifiers,
716706
major=self.num_major,
717707
minor=self.num_minor,
718708
point=self.num_point,
719-
flat=flat,
720709
)
721-
version_strings = [v._string for v in active_versions]
722-
return version_strings
710+
711+
def get_stable_version(self):
712+
return self.versions.filter(slug=STABLE).first()
713+
714+
def update_stable_version(self):
715+
"""
716+
Returns the version that was promoited to be the new stable version.
717+
It will return ``None`` if no update was mode or if there is no
718+
version on the project that can be considered stable.
719+
"""
720+
versions = self.versions.all()
721+
new_stable = determine_stable_version(versions)
722+
if new_stable:
723+
current_stable = self.get_stable_version()
724+
if current_stable:
725+
identifier_updated = (
726+
new_stable.identifier != current_stable.identifier)
727+
if identifier_updated and current_stable.machine:
728+
log.info(
729+
"Update stable version: {project}:{version}".format(
730+
project=self.slug,
731+
version=new_stable.identifier))
732+
current_stable.identifier = new_stable.identifier
733+
current_stable.save()
734+
return new_stable
735+
else:
736+
log.info(
737+
"Creating new stable version: {project}:{version}".format(
738+
project=self.slug,
739+
version=new_stable.identifier))
740+
current_stable = self.versions.create_stable(
741+
type=new_stable.type,
742+
identifier=new_stable.identifier)
743+
return new_stable
723744

724745
def version_from_branch_name(self, branch):
746+
versions = self.versions_from_branch_name(branch)
725747
try:
726-
return (
727-
self.versions.filter(identifier=branch) |
728-
self.versions.filter(identifier=('remotes/origin/%s' % branch)) |
729-
self.versions.filter(identifier=('origin/%s' % branch))
730-
)[0]
748+
return versions[0]
731749
except IndexError:
732750
return None
733751

readthedocs/projects/templatetags/projects_tags.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,20 @@
11
from django import template
22

3-
from distutils2.version import NormalizedVersion
4-
from projects.utils import mkversion
5-
from builds.constants import LATEST
6-
from builds.constants import STABLE
7-
8-
register = template.Library()
3+
from projects.version_handling import comparable_version
94

105

11-
def make_version(version):
12-
ver = mkversion(version)
13-
if not ver:
14-
if version.slug == LATEST:
15-
return NormalizedVersion('99999.0', error_on_huge_major_num=False)
16-
elif version.slug == STABLE:
17-
return NormalizedVersion('9999.0', error_on_huge_major_num=False)
18-
else:
19-
return NormalizedVersion('999.0', error_on_huge_major_num=False)
20-
return ver
6+
register = template.Library()
217

228

239
@register.filter
2410
def sort_version_aware(versions):
2511
"""
2612
Takes a list of versions objects and sort them caring about version schemes
2713
"""
28-
sorted_verisons = sorted(versions,
29-
key=make_version,
30-
reverse=True)
31-
return sorted_verisons
14+
return sorted(
15+
versions,
16+
key=lambda version: comparable_version(version.verbose_name),
17+
reverse=True)
3218

3319

3420
@register.filter

readthedocs/projects/utils.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from httplib2 import Http
1010

1111
from django.conf import settings
12-
from distutils2.version import NormalizedVersion, suggest_normalized_version
1312
import redis
1413

1514
from builds.constants import LATEST
@@ -114,37 +113,6 @@ def safe_write(filename, contents):
114113
fh.close()
115114

116115

117-
def mkversion(version_obj):
118-
try:
119-
if hasattr(version_obj, 'slug'):
120-
ver = NormalizedVersion(
121-
suggest_normalized_version(version_obj.slug)
122-
)
123-
else:
124-
ver = NormalizedVersion(
125-
suggest_normalized_version(version_obj['slug'])
126-
)
127-
return ver
128-
except TypeError:
129-
return None
130-
131-
132-
def highest_version(version_list):
133-
highest = [None, None]
134-
for version in version_list:
135-
ver = mkversion(version)
136-
if not ver:
137-
continue
138-
elif highest[1] and ver:
139-
# If there's a highest, and no version, we don't need to set
140-
# anything
141-
if ver > highest[1]:
142-
highest = [version, ver]
143-
else:
144-
highest = [version, ver]
145-
return highest
146-
147-
148116
def purge_version(version, mainsite=False, subdomain=False, cname=False):
149117
varnish_servers = getattr(settings, 'VARNISH_SERVERS', None)
150118
h = Http()

0 commit comments

Comments
 (0)