Skip to content

Addons: sorting algorithm for versions customizable on flyout #11069

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions readthedocs/projects/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,23 @@
_("Single version without translations (/<filename>)"),
),
)


ADDONS_FLYOUT_SORTING_LEXICOGRAPHYCALLY = "alphabetically"
# Compatibility to keep the behavior of the old flyout.
# This isn't a good algorithm, but it's a way to keep the old behavior in case we need it.
ADDONS_FLYOUT_SORTING_SEMVER_READTHEDOCS_COMPATIBLE = "semver-readthedocs-compatible"
# https://pypi.org/project/packaging/
ADDONS_FLYOUT_SORTING_PYTHON_PACKAGING = "python-packaging"
ADDONS_FLYOUT_SORTING_CALVER = "calver"
# Let the user to define a custom pattern and use BumpVer to parse and sort the versions.
# https://github.com/mbarkhau/bumpver#pattern-examples
ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN = "custom-pattern"

ADDONS_FLYOUT_SORTING_CHOICES = (
(ADDONS_FLYOUT_SORTING_LEXICOGRAPHYCALLY, "Alphabetically"),
(ADDONS_FLYOUT_SORTING_SEMVER_READTHEDOCS_COMPATIBLE, "SemVer (Read the Docs)"),
(ADDONS_FLYOUT_SORTING_PYTHON_PACKAGING, "Python Packaging (PEP 440 and PEP 425)"),
(ADDONS_FLYOUT_SORTING_CALVER, "CalVer (YYYY.0M.0M)"),
(ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN, "Define your own pattern"),
)
15 changes: 15 additions & 0 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from readthedocs.integrations.models import Integration
from readthedocs.invitations.models import Invitation
from readthedocs.oauth.models import RemoteRepository
from readthedocs.projects.constants import ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN
from readthedocs.projects.models import (
AddonsConfig,
Domain,
Expand Down Expand Up @@ -498,6 +499,8 @@ class Meta:
"doc_diff_enabled",
"external_version_warning_enabled",
"flyout_enabled",
"flyout_sorting",
"flyout_sorting_custom_pattern",
"hotkeys_enabled",
"search_enabled",
"stable_latest_version_warning_enabled",
Expand All @@ -522,6 +525,18 @@ def __init__(self, *args, **kwargs):
kwargs["instance"] = addons
super().__init__(*args, **kwargs)

def clean(self):
if (
self.cleaned_data["flyout_sorting"] == ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN
and not self.cleaned_data["flyout_sorting_custom_pattern"]
):
raise forms.ValidationError(
_(
"The flyout sorting custom pattern is required when selecting a custom pattern."
),
)
return super().clean()

def clean_project(self):
return self.project

Expand Down
38 changes: 38 additions & 0 deletions readthedocs/projects/migrations/0114_addons_flyout_sorting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 4.2.9 on 2024-01-26 12:08

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("projects", "0113_disable_analytics_addons"),
]

operations = [
migrations.AddField(
model_name="addonsconfig",
name="flyout_sorting_custom_pattern",
field=models.CharField(
blank=True,
default=None,
help_text="Sorting pattern supported by BumpVer "
'(<a href="https://github.com/mbarkhau/bumpver#pattern-examples"> See examples</a>',
max_length=32,
null=True,
),
),
migrations.AlterField(
model_name="addonsconfig",
name="flyout_sorting",
field=models.CharField(
choices=[
("alphabetically", "Alphabetically"),
("semver-readthedocs-compatible", "SemVer (Read the Docs)"),
("python-packaging", "Python Packaging (PEP 440 and PEP 425)"),
("calver", "CalVer (YYYY.0M.0M)"),
("custom-pattern", "Define your own pattern"),
],
default="alphabetically",
),
),
]
14 changes: 14 additions & 0 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
from readthedocs.vcs_support.backends import backend_cls

from .constants import (
ADDONS_FLYOUT_SORTING_CHOICES,
ADDONS_FLYOUT_SORTING_LEXICOGRAPHYCALLY,
DOWNLOADABLE_MEDIA_TYPES,
MEDIA_TYPES,
MULTIPLE_VERSIONS_WITH_TRANSLATIONS,
Expand Down Expand Up @@ -179,6 +181,18 @@ class AddonsConfig(TimeStampedModel):

# Flyout
flyout_enabled = models.BooleanField(default=True)
flyout_sorting = models.CharField(
choices=ADDONS_FLYOUT_SORTING_CHOICES,
default=ADDONS_FLYOUT_SORTING_LEXICOGRAPHYCALLY,
)
flyout_sorting_custom_pattern = models.CharField(
max_length=32,
default=None,
null=True,
blank=True,
help_text="Sorting pattern supported by BumpVer "
'(<a href="https://github.com/mbarkhau/bumpver#pattern-examples"> See examples</a>',
)

# Hotkeys
hotkeys_enabled = models.BooleanField(default=True)
Expand Down
88 changes: 87 additions & 1 deletion readthedocs/projects/version_handling.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Project version handling."""
import operator
import unicodedata

from packaging.version import InvalidVersion, Version
from bumpver.v2version import parse_version_info
from bumpver.version import PatternError
from packaging.version import InvalidVersion, Version, parse

from readthedocs.builds.constants import LATEST_VERBOSE_NAME, STABLE_VERBOSE_NAME, TAG
from readthedocs.vcs_support.backends import backend_cls
Expand Down Expand Up @@ -158,3 +161,86 @@ def determine_stable_version(version_list):
version_obj, comparable = versions[0]
return version_obj
return None


def sort_versions_python_packaging(version_list):
"""
Sort Read the Docs versions list using ``packaging`` algorithm.

All the invalid version (raise ``InvalidVersion``) are added at the end
sorted alphabetically.

https://pypi.org/project/packaging/
https://packaging.python.org/en/latest/specifications/version-specifiers/
"""
alphabetically_sorted_version_list = sorted(
version_list,
key=operator.attrgetter("slug"),
)

valid_versions = []
invalid_versions = []
for i, version in enumerate(alphabetically_sorted_version_list):
try:
valid_versions.append((version, Version(version.slug)))
except InvalidVersion:
# When the version is invalid, we put it at the end while keeping
# the alphabetically sorting between the invalid ones.
invalid_versions.append((version, parse(str(100000 + i))))

return [
item[0]
for item in sorted(valid_versions, key=operator.itemgetter(1))
+ invalid_versions
]


def sort_versions_calver(version_list):
"""
Sort Read the Docs versions using CalVer pattern: ``YYYY.0M.0M``.

All the invalid version are added at the end sorted alphabetically.
"""
raw_pattern = "YYYY.0M.0D"
return sort_versions_custom_pattern(version_list, raw_pattern)


def sort_versions_custom_pattern(version_list, raw_pattern):
"""
Sort Read the Docs versions using a custom pattern.

All the invalid version (raise ``PatternError``) are added at the end
sorted alphabetically.

It uses ``Bumpver`` behinds the scenes for the parsing and sorting.
https://github.com/mbarkhau/bumpver
"""
raw_pattern = "YYYY.0M.0D"
alphabetically_sorted_version_list = sorted(
version_list,
key=operator.attrgetter("slug"),
)

valid_versions = []
invalid_versions = []
for i, version in enumerate(alphabetically_sorted_version_list):
try:
valid_versions.append(
(
version,
parse_version_info(
version.slug,
raw_pattern=raw_pattern,
),
)
)
except PatternError:
# When the version is invalid, we put it at the end while keeping
# the alphabetically sorting between the invalid ones.
invalid_versions.append((version, parse(str(100000 + i))))

return [
item[0]
for item in sorted(valid_versions, key=operator.itemgetter(1))
+ invalid_versions
]
44 changes: 41 additions & 3 deletions readthedocs/proxito/views/hosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,19 @@
from readthedocs.core.resolver import Resolver
from readthedocs.core.unresolver import UnresolverError, unresolver
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects.constants import (
ADDONS_FLYOUT_SORTING_CALVER,
ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN,
ADDONS_FLYOUT_SORTING_PYTHON_PACKAGING,
ADDONS_FLYOUT_SORTING_SEMVER_READTHEDOCS_COMPATIBLE,
)
from readthedocs.projects.models import AddonsConfig, Project
from readthedocs.projects.version_handling import (
comparable_version,
sort_versions_calver,
sort_versions_custom_pattern,
sort_versions_python_packaging,
)

log = structlog.get_logger(__name__) # noqa

Expand Down Expand Up @@ -265,6 +277,32 @@ def _v0(self, project, version, build, filename, url, user):
.only("slug", "type")
.order_by("slug")
)
if (
project.addons.flyout_sorting
== ADDONS_FLYOUT_SORTING_SEMVER_READTHEDOCS_COMPATIBLE
):
versions_active_built_not_hidden = sorted(
versions_active_built_not_hidden,
key=lambda version: comparable_version(
version.verbose_name,
repo_type=project.repo_type,
),
)
elif (
project.addons.flyout_sorting == ADDONS_FLYOUT_SORTING_PYTHON_PACKAGING
):
versions_active_built_not_hidden = sort_versions_python_packaging(
versions_active_built_not_hidden
)
elif project.addons.flyout_sorting == ADDONS_FLYOUT_SORTING_CALVER:
versions_active_built_not_hidden = sort_versions_calver(
versions_active_built_not_hidden
)
elif project.addons.flyout_sorting == ADDONS_FLYOUT_SORTING_CUSTOM_PATTERN:
versions_active_built_not_hidden = sort_versions_custom_pattern(
versions_active_built_not_hidden,
project.addons.flyout_sorting_custom_pattern,
)

if version:
version_downloads = version.get_downloads(pretty=True).items()
Expand Down Expand Up @@ -332,9 +370,9 @@ def _v0(self, project, version, build, filename, url, user):
# NOTE: I think we are moving away from these selectors
# since we are doing floating noticications now.
# "query_selector": "[role=main]",
"versions": list(
versions_active_built_not_hidden.values_list("slug", flat=True)
),
"versions": [
version_.slug for version_ in versions_active_built_not_hidden
],
},
"flyout": {
"enabled": project.addons.flyout_enabled,
Expand Down
26 changes: 21 additions & 5 deletions requirements/deploy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --output-file=requirements/deploy.txt requirements/deploy.in
# pip-compile --output-file=requirements/deploy.txt --resolver=backtracking requirements/deploy.in
#
amqp==5.2.0
# via
Expand Down Expand Up @@ -32,6 +32,8 @@ botocore==1.34.29
# -r requirements/pip.txt
# boto3
# s3transfer
bumpver==2023.1129
# via -r requirements/pip.txt
celery==5.2.7
# via
# -r requirements/pip.txt
Expand All @@ -53,6 +55,7 @@ charset-normalizer==3.3.2
click==8.1.7
# via
# -r requirements/pip.txt
# bumpver
# celery
# click-didyoumean
# click-plugins
Expand All @@ -69,6 +72,10 @@ click-repl==0.3.0
# via
# -r requirements/pip.txt
# celery
colorama==0.4.6
# via
# -r requirements/pip.txt
# bumpver
cron-descriptor==1.4.0
# via
# -r requirements/pip.txt
Expand Down Expand Up @@ -153,9 +160,7 @@ django-polymorphic==3.1.0
django-simple-history==3.0.0
# via -r requirements/pip.txt
django-storages[boto3]==1.14.2
# via
# -r requirements/pip.txt
# django-storages
# via -r requirements/pip.txt
django-structlog==2.2.0
# via -r requirements/pip.txt
django-taggit==5.0.1
Expand Down Expand Up @@ -232,6 +237,14 @@ kombu==5.3.5
# via
# -r requirements/pip.txt
# celery
lexid==2021.1006
# via
# -r requirements/pip.txt
# bumpver
looseversion==1.3.0
# via
# -r requirements/pip.txt
# bumpver
lxml==5.1.0
# via
# -r requirements/pip.txt
Expand Down Expand Up @@ -286,7 +299,6 @@ pyjwt[crypto]==2.8.0
# via
# -r requirements/pip.txt
# django-allauth
# pyjwt
pyquery==2.0.0
# via -r requirements/pip.txt
python-crontab==3.0.0
Expand Down Expand Up @@ -369,6 +381,10 @@ structlog==23.2.0
# structlog-sentry
structlog-sentry==2.0.3
# via -r requirements/deploy.in
toml==0.10.2
# via
# -r requirements/pip.txt
# bumpver
tomli==2.0.1
# via
# -r requirements/pip.txt
Expand Down
Loading