5050
5151TYPE_CHECKING = False
5252if TYPE_CHECKING:
53- from collections.abc import Sequence
53+ from collections.abc import Iterator, Sequence
5454 from typing import Literal, TypeAlias
5555
56- Versions: TypeAlias = Sequence["Version"]
5756 Languages: TypeAlias = Sequence["Language"]
5857
5958try:
7170HERE = Path(__file__).resolve().parent
7271
7372
73+ @dataclass(frozen=True, slots=True)
74+ class Versions:
75+ _seq: Sequence[Version]
76+
77+ def __iter__(self) -> Iterator[Version]:
78+ return iter(self._seq)
79+
80+ def __reversed__(self) -> Iterator[Version]:
81+ return reversed(self._seq)
82+
83+ @classmethod
84+ def from_json(cls, data) -> Versions:
85+ versions = sorted(
86+ [Version.from_json(name, release) for name, release in data.items()],
87+ key=Version.as_tuple,
88+ )
89+ return cls(versions)
90+
91+ def filter(self, branch: str = "") -> Sequence[Version]:
92+ """Filter the given versions.
93+
94+ If *branch* is given, only *versions* matching *branch* are returned.
95+
96+ Else all live versions are returned (this means no EOL and no
97+ security-fixes branches).
98+ """
99+ if branch:
100+ return [v for v in self if branch in (v.name, v.branch_or_tag)]
101+ return [v for v in self if v.status not in {"EOL", "security-fixes"}]
102+
103+ @property
104+ def current_stable(self) -> Version:
105+ """Find the current stable CPython version."""
106+ return max((v for v in self if v.status == "stable"), key=Version.as_tuple)
107+
108+ @property
109+ def current_dev(self) -> Version:
110+ """Find the current CPython version in development."""
111+ return max(self, key=Version.as_tuple)
112+
113+ def setup_indexsidebar(self, current: Version, dest_path: Path) -> None:
114+ """Build indexsidebar.html for Sphinx."""
115+ template_path = HERE / "templates" / "indexsidebar.html"
116+ template = jinja2.Template(template_path.read_text(encoding="UTF-8"))
117+ rendered_template = template.render(
118+ current_version=current,
119+ versions=list(reversed(self)),
120+ )
121+ dest_path.write_text(rendered_template, encoding="UTF-8")
122+
123+
74124@total_ordering
75125class Version:
76126 """Represents a CPython version and its documentation build dependencies."""
@@ -101,6 +151,17 @@ def __init__(self, name, *, status, branch_or_tag=None):
101151 def __repr__(self):
102152 return f"Version({self.name})"
103153
154+ def __eq__(self, other):
155+ return self.name == other.name
156+
157+ def __gt__(self, other):
158+ return self.as_tuple() > other.as_tuple()
159+
160+ @classmethod
161+ def from_json(cls, name, values):
162+ """Loads a version from devguide's json representation."""
163+ return cls(name, status=values["status"], branch_or_tag=values["branch"])
164+
104165 @property
105166 def requirements(self):
106167 """Generate the right requirements for this version.
@@ -144,29 +205,6 @@ def title(self):
144205 """The title of this version's doc, for the sidebar."""
145206 return f"Python {self.name} ({self.status})"
146207
147- @staticmethod
148- def filter(versions, branch=None):
149- """Filter the given versions.
150-
151- If *branch* is given, only *versions* matching *branch* are returned.
152-
153- Else all live versions are returned (this means no EOL and no
154- security-fixes branches).
155- """
156- if branch:
157- return [v for v in versions if branch in (v.name, v.branch_or_tag)]
158- return [v for v in versions if v.status not in ("EOL", "security-fixes")]
159-
160- @staticmethod
161- def current_stable(versions):
162- """Find the current stable CPython version."""
163- return max((v for v in versions if v.status == "stable"), key=Version.as_tuple)
164-
165- @staticmethod
166- def current_dev(versions):
167- """Find the current CPython version in development."""
168- return max(versions, key=Version.as_tuple)
169-
170208 @property
171209 def picker_label(self):
172210 """Forge the label of a version picker."""
@@ -176,27 +214,6 @@ def picker_label(self):
176214 return f"pre ({self.name})"
177215 return self.name
178216
179- def setup_indexsidebar(self, versions: Versions, dest_path: Path):
180- """Build indexsidebar.html for Sphinx."""
181- template_path = HERE / "templates" / "indexsidebar.html"
182- template = jinja2.Template(template_path.read_text(encoding="UTF-8"))
183- rendered_template = template.render(
184- current_version=self,
185- versions=versions[::-1],
186- )
187- dest_path.write_text(rendered_template, encoding="UTF-8")
188-
189- @classmethod
190- def from_json(cls, name, values):
191- """Loads a version from devguide's json representation."""
192- return cls(name, status=values["status"], branch_or_tag=values["branch"])
193-
194- def __eq__(self, other):
195- return self.name == other.name
196-
197- def __gt__(self, other):
198- return self.as_tuple() > other.as_tuple()
199-
200217
201218@dataclass(order=True, frozen=True, kw_only=True)
202219class Language:
@@ -619,8 +636,8 @@ def build(self):
619636 + ([""] if sys.platform == "darwin" else [])
620637 + ["s/ *-A switchers=1//", self.checkout / "Doc" / "Makefile"]
621638 )
622- self.version .setup_indexsidebar(
623- self.versions ,
639+ self.versions .setup_indexsidebar(
640+ self.version ,
624641 self.checkout / "Doc" / "tools" / "templates" / "indexsidebar.html",
625642 )
626643 run_with_logging(
@@ -1013,7 +1030,7 @@ def build_docs(args) -> bool:
10131030 # This runs languages in config.toml order and versions newest first.
10141031 todo = [
10151032 (version, language)
1016- for version in Version .filter(versions, args.branch)
1033+ for version in versions .filter(args.branch)
10171034 for language in reversed(Language.filter(languages, args.languages))
10181035 ]
10191036 del args.branch
@@ -1081,9 +1098,7 @@ def parse_versions_from_devguide(http: urllib3.PoolManager) -> Versions:
10811098 "python/devguide/main/include/release-cycle.json",
10821099 timeout=30,
10831100 ).json()
1084- versions = [Version.from_json(name, release) for name, release in releases.items()]
1085- versions.sort(key=Version.as_tuple)
1086- return versions
1101+ return Versions.from_json(releases)
10871102
10881103
10891104def parse_languages_from_config() -> Languages:
@@ -1170,7 +1185,7 @@ def major_symlinks(
11701185 - /es/3/ → /es/3.9/
11711186 """
11721187 logging.info("Creating major version symlinks...")
1173- current_stable = Version .current_stable(versions) .name
1188+ current_stable = versions .current_stable.name
11741189 for language in languages:
11751190 symlink(
11761191 www_root,
@@ -1200,7 +1215,7 @@ def dev_symlink(
12001215 - /es/dev/ → /es/3.11/
12011216 """
12021217 logging.info("Creating development version symlinks...")
1203- current_dev = Version .current_dev(versions) .name
1218+ current_dev = versions .current_dev.name
12041219 for language in languages:
12051220 symlink(
12061221 www_root,
0 commit comments