test_ok2/scripts/update-plugin-list.py

143 lines
4.2 KiB
Python
Raw Normal View History

2021-01-29 22:19:54 +08:00
import datetime
import pathlib
import re
from textwrap import dedent
from textwrap import indent
2021-01-29 22:19:54 +08:00
import packaging.version
import requests
import tabulate
import wcwidth
from tqdm import tqdm
2021-01-29 22:19:54 +08:00
FILE_HEAD = r"""
.. _plugin-list:
Plugin List
===========
2021-01-29 22:19:54 +08:00
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
page.
2021-01-29 22:19:54 +08:00
"""
DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 1 - Planning",
"Development Status :: 2 - Pre-Alpha",
"Development Status :: 3 - Alpha",
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Development Status :: 6 - Mature",
"Development Status :: 7 - Inactive",
)
def escape_rst(text: str) -> str:
"""Rudimentary attempt to escape special RST characters to appear as
plain text."""
text = (
text.replace("*", "\\*")
.replace("<", "\\<")
.replace(">", "\\>")
.replace("`", "\\`")
)
text = re.sub(r"_\b", "", text)
return text
2021-01-29 22:19:54 +08:00
def iter_plugins():
regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple")
matches = list(
match
for match in re.finditer(regex, response.text)
if match.groups()[0].startswith("pytest-")
)
for match in tqdm(matches, smoothing=0):
2021-01-29 22:19:54 +08:00
name = match.groups()[0]
response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
# return 404 on the JSON API. Skip.
continue
response.raise_for_status()
info = response.json()["info"]
if "Development Status :: 7 - Inactive" in info["classifiers"]:
continue
for classifier in DEVELOPMENT_STATUS_CLASSIFIERS:
if classifier in info["classifiers"]:
status = classifier[22:]
break
else:
status = "N/A"
requires = "N/A"
if info["requires_dist"]:
for requirement in info["requires_dist"]:
if re.match(r"pytest(?![-.\w])", requirement):
2021-01-29 22:19:54 +08:00
requires = requirement
break
releases = response.json()["releases"]
for release in sorted(releases, key=packaging.version.parse, reverse=True):
if releases[release]:
release_date = datetime.date.fromisoformat(
releases[release][-1]["upload_time_iso_8601"].split("T")[0]
)
last_release = release_date.strftime("%b %d, %Y")
break
name = f':pypi:`{info["name"]}`'
summary = ""
if info["summary"]:
summary = escape_rst(info["summary"].replace("\n", ""))
2021-01-29 22:19:54 +08:00
yield {
"name": name,
"summary": summary.strip(),
2021-01-29 22:19:54 +08:00
"last release": last_release,
"status": status,
"requires": requires,
}
def plugin_definitions(plugins):
"""Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins:
yield dedent(
f"""
{plugin['name']}
*last release*: {plugin["last release"]},
*status*: {plugin["status"]},
*requires*: {plugin["requires"]}
{plugin["summary"]}
"""
)
2021-01-29 22:19:54 +08:00
def main():
plugins = list(iter_plugins())
reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst"
2021-01-29 22:19:54 +08:00
with plugin_list.open("w") as f:
f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n")
wcwidth # reference library that must exist for tabulate to work
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
f.write(indent(plugin_table, " "))
f.write("\n\n")
f.write(".. only:: latex\n\n")
f.write(indent("".join(plugin_definitions(plugins)), " "))
2021-01-29 22:19:54 +08:00
if __name__ == "__main__":
main()