Skip to content

Commit 005d398

Browse files
authored
Fix plugin list discovery and updates (#978)
1 parent b742daa commit 005d398

3 files changed

Lines changed: 63 additions & 23 deletions

File tree

‎.github/workflows/update-plugin-list.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,5 @@ jobs:
3939
author: 'Tobias Raabe <tobiasraabe@users.noreply.github.com>'
4040
branch: update-plugin-list/patch
4141
delete-branch: true
42-
branch-suffix: short-commit-hash
4342
title: '[automated] Update plugin list'
4443
body: '[automated] Update plugin list'

‎docs/source/plugin_list.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ Packages classified as inactive are excluded.
1313
conduct your own quality assessment before incorporating any of these plugins into your
1414
own projects.
1515

16-
This list contains 5 plugins.
16+
This list contains 6 plugins.
1717

18-
| name | summary | last release | status | requires |
19-
| -------------------------------------------------------- | ------------------------------------------ | ------------ | --------- | -------------- |
20-
| [pytask-julia](https://pypi.org/project/pytask-julia/) | A Pytask plugin for Julia | Oct 07, 2023 | 4 - Beta | pytask >=0.4.0 |
21-
| [pytask-latex](https://pypi.org/project/pytask-latex/) | Compile LaTeX documents with pytask. | Jun 01, 2025 | 4 - Beta | pytask>=0.4.0 |
22-
| [pytask-r](https://pypi.org/project/pytask-r/) | Run R scripts with pytask. | Apr 20, 2024 | 4 - Beta | pytask>=0.4.5 |
23-
| [pytask-stata](https://pypi.org/project/pytask-stata/) | Execute do-files with Stata and pytask. | Jul 26, 2025 | 4 - Beta | pytask>=0.5.2 |
24-
| [pytask-vscode](https://pypi.org/project/pytask-vscode/) | Additional Logging for VS Code integration | Nov 21, 2023 | 3 - Alpha | pytask >=0.4.2 |
18+
| name | summary | last release | status | requires |
19+
| ------------------------------------------------------------ | ----------------------------------------------- | ------------ | --------- | -------------- |
20+
| [pytask-julia](https://pypi.org/project/pytask-julia/) | A Pytask plugin for Julia | Oct 07, 2023 | 4 - Beta | pytask >=0.4.0 |
21+
| [pytask-latex](https://pypi.org/project/pytask-latex/) | Compile LaTeX documents with pytask. | Jun 01, 2025 | 4 - Beta | pytask>=0.4.0 |
22+
| [pytask-parallel](https://pypi.org/project/pytask-parallel/) | Parallelize the execution of tasks with pytask. | Aug 23, 2026 | 4 - Beta | pytask>=0.5.2 |
23+
| [pytask-r](https://pypi.org/project/pytask-r/) | Run R scripts with pytask. | Apr 20, 2024 | 4 - Beta | pytask>=0.4.5 |
24+
| [pytask-stata](https://pypi.org/project/pytask-stata/) | Execute do-files with Stata and pytask. | Jun 14, 2026 | 4 - Beta | pytask>=0.5.2 |
25+
| [pytask-vscode](https://pypi.org/project/pytask-vscode/) | Additional Logging for VS Code integration | Nov 21, 2023 | 3 - Alpha | pytask >=0.4.2 |

‎scripts/update_plugin_list.py‎

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@
3232
from __future__ import annotations
3333

3434
import datetime
35+
import html.parser
3536
import pathlib
3637
import re
3738
from typing import TYPE_CHECKING
39+
from urllib.parse import unquote
40+
from urllib.parse import urlparse
3841

3942
import httpx
4043
import packaging.version
4144
import wcwidth
45+
from packaging.utils import canonicalize_name
4246

4347
if TYPE_CHECKING:
4448
from collections.abc import Generator
@@ -122,27 +126,61 @@ def _create_table(entries: list[dict[str, str]]) -> str:
122126
return "\n".join([header, separator, *rows])
123127

124128

129+
class _SimpleIndexParser(html.parser.HTMLParser):
130+
"""Extract normalized project names from the PyPI simple index."""
131+
132+
def __init__(self) -> None:
133+
super().__init__()
134+
self.project_names: list[str] = []
135+
136+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
137+
if tag != "a":
138+
return
139+
140+
href = dict(attrs).get("href")
141+
if href is None:
142+
return
143+
144+
path = urlparse(href).path.rstrip("/")
145+
if not path.startswith("/simple/"):
146+
return
147+
148+
project_name = unquote(path.removeprefix("/simple/"))
149+
if "/" not in project_name and project_name:
150+
self.project_names.append(canonicalize_name(project_name))
151+
152+
153+
def _iter_project_names(simple_index: str) -> list[str]:
154+
"""Return normalized project names from a PyPI simple index response."""
155+
parser = _SimpleIndexParser()
156+
parser.feed(simple_index)
157+
parser.close()
158+
return parser.project_names
159+
160+
125161
def _iter_plugins() -> Generator[dict[str, str], None, None]: # noqa: C901
126162
"""Iterate over all plugins and format entries."""
127-
regex = r">([\d\w-]*)</a>"
128163
response = httpx.get("https://pypi.org/simple/", timeout=20)
164+
response.raise_for_status()
129165

130-
matches = [
131-
match
132-
for match in re.finditer(regex, response.text)
133-
if match.groups()[0].startswith("pytask-")
134-
and match.groups()[0] not in _EXCLUDED_PACKAGES
166+
project_names = [
167+
project_name
168+
for project_name in _iter_project_names(response.text)
169+
if project_name.startswith("pytask-") and project_name not in _EXCLUDED_PACKAGES
135170
]
136171

137-
for match in matches:
138-
name = match.groups()[0]
139-
response = httpx.get(f"https://pypi.org/pypi/{name}/json", timeout=20)
140-
if response.status_code == 404: # noqa: PLR2004
172+
for project_name in project_names:
173+
package_response = httpx.get(
174+
f"https://pypi.org/pypi/{project_name}/json",
175+
timeout=20,
176+
)
177+
if package_response.status_code == 404: # noqa: PLR2004
141178
# Some packages might return a 404.
142179
continue
143180

144-
response.raise_for_status()
145-
info = response.json()["info"]
181+
package_response.raise_for_status()
182+
package = package_response.json()
183+
info = package["info"]
146184

147185
if "Development Status :: 7 - Inactive" in info["classifiers"]:
148186
continue
@@ -171,7 +209,7 @@ def _version_sort_key(version_string: str) -> packaging.version.Version:
171209
# Use a hard-coded pre-release version.
172210
return packaging.version.Version("0.0.0alpha")
173211

174-
releases = response.json()["releases"]
212+
releases = package["releases"]
175213

176214
for release in sorted(releases, key=_version_sort_key, reverse=True):
177215
if releases[release]:
@@ -181,7 +219,9 @@ def _version_sort_key(version_string: str) -> packaging.version.Version:
181219
last_release = release_date.strftime("%b %d, %Y")
182220
break
183221

184-
name = f"[{info['name']}](https://pypi.org/project/{info['name']}/)"
222+
# Use the canonical name from the simple-index link. Some projects have
223+
# temporarily published metadata with a different name, e.g. pytask_stata.
224+
name = f"[{project_name}](https://pypi.org/project/{project_name}/)"
185225
summary = ""
186226
if info["summary"]:
187227
summary = _escape_markdown(info["summary"].replace("\n", ""))

0 commit comments

Comments
 (0)