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 {latest}.' ).format(latest=latest.name, href=latest.href(context)) return ( "This is an old version. The latest stable version is" ' {latest}.' ).format(latest=latest.name, href=latest.href(context))