summaryrefslogtreecommitdiffstats
path: root/src/pallets_sphinx_themes/versions.py
blob: 62c77e465e0486a35370acb5a3fcd741ac622075 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import json
import os
from collections import namedtuple

from jinja2 import pass_context
from packaging import version as pv

from .theme_check import only_pallets_theme


@only_pallets_theme()
def load_versions(app):
    if os.environ.get("READTHEDOCS"):
        versions = readthedocs_versions(app)
    else:
        versions = local_versions(app)

    context = app.config.html_context
    context["versions"] = versions
    context["current_version"] = next((v for v in versions if v.current), None)
    context["latest_version"] = next((v for v in versions if v.latest), None)


def local_versions(app):
    config_versions = app.config.html_context.get("versions")

    if isinstance(config_versions, str):
        if os.path.isfile(config_versions):
            with open(config_versions, encoding="utf8") as f:
                config_versions = json.load(f)
        else:
            config_versions = json.loads(config_versions)

    if not config_versions:
        return []

    versions = []

    for version in config_versions:
        if isinstance(version, dict):
            version = DocVersion(**version)

        versions.append(version)

    slug = app.config.version
    dev = "dev" in app.config.release
    seen_latest = False

    for i, version in enumerate(versions):
        if version.slug == "dev":
            versions[i] = version._replace(dev=True, current=dev)

        if version.slug == slug:
            versions[i] = version._replace(current=True)

        if not seen_latest and version.version is not None:
            seen_latest = True
            versions[i] = version._replace(latest=True)

    return versions


def readthedocs_versions(app):
    config_versions = app.config.html_context["versions"]
    current_slug = app.config.html_context["current_version"]
    number_versions = []
    name_versions = []

    for slug, _ in config_versions:
        dev = slug in {"main", "master", "default", "latest"}
        version = _parse_version(slug)

        if version is not None:
            name = slug
            append_to = number_versions
        else:
            name = "Development" if dev else slug.title()
            append_to = name_versions

        append_to.append(
            DocVersion(name=name, slug=slug, dev=dev, current=slug == current_slug)
        )

    # put the newest numbered version first
    number_versions.sort(key=lambda x: x.version, reverse=True)
    # put non-dev named versions first
    name_versions.sort(key=lambda x: x.dev, reverse=True)
    versions = number_versions + name_versions

    # if there are non-dev versions, mark the newest one as the latest
    if versions and not versions[0].dev:
        versions[0] = versions[0]._replace(latest=True)

    return versions


def _parse_version(value: str, placeholder: str = "x"):
    if value.endswith(f".{placeholder}"):
        value = value[: -(len(placeholder) + 1)]

    try:
        return pv.Version(value)
    except pv.InvalidVersion:
        return None


class DocVersion(
    namedtuple("DocVersion", ("name", "slug", "version", "latest", "dev", "current"))
):
    __slots__ = ()

    def __new__(cls, name, slug=None, latest=False, dev=False, current=False):
        slug = slug or name
        version = _parse_version(slug)

        if version is not None:
            name = "Version " + name

        return super().__new__(cls, name, slug, version, latest, dev, current)

    @pass_context
    def href(self, context):
        pathto = context["pathto"]
        master_doc = context["master_doc"]
        pagename = context["pagename"]

        builder = pathto.__closure__[0].cell_contents
        master = pathto(master_doc).rstrip("#/") or "."
        path = builder.get_target_uri(pagename)
        return "/".join((master, "..", self.slug, path))

    @pass_context
    def banner(self, context):
        if self.latest:
            return

        latest = context["latest_version"]

        # Don't show a banner if the latest version couldn't be determined, or if this
        # is the "stable" version.
        if latest is None or self.name == "stable":
            return

        if self.dev:
            return (
                "This is the development version. The latest stable"
                ' version is <a href="{href}">{latest}</a>.'
            ).format(latest=latest.name, href=latest.href(context))

        return (
            "This is an old version. The latest stable version is"
            ' <a href="{href}">{latest}</a>.'
        ).format(latest=latest.name, href=latest.href(context))