Merge branch 'fix/todo-pass2'

This commit is contained in:
Vinta Chen
2026-09-27 15:11:22 +08:00
15 changed files with 92 additions and 63 deletions
+43 -45
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -755,8 +755,9 @@ def build(repo_root: Path) -> None:
if parent_category:
breadcrumbs.append((parent_category["name"], category_public_url(parent_category)))
breadcrumbs.append((category["name"], category_url))
page_entries = [entry for group in entry_groups for entry in group["entries"]] if entry_groups else entries
category_json_ld = json.dumps(
build_category_json_ld(category_title.removesuffix(" - Awesome Python"), category_url, category_description, entries, breadcrumbs),
build_category_json_ld(category_title.removesuffix(" - Awesome Python"), category_url, category_description, page_entries, breadcrumbs),
ensure_ascii=False,
).replace("</", "<\\/")
(page_dir / "index.html").write_text(
@@ -1,4 +1,4 @@
Validate API input and config with Pydantic, the Python data validation library built on type hints. Use Pandera for dataframes, jsonschema for JSON Schema.
Declare API input and config as type hints, and Pydantic validates them. Pandera brings Python data validation to dataframes, and jsonschema covers JSON Schema.
How to choose:
@@ -1,4 +1,4 @@
Most code should handle time zones with zoneinfo and pick python-dateutil, the Python date library for parsing date strings and adding months.
Handle time zones with the built-in zoneinfo. Parsing date strings and adding months is where a Python date library earns its install: python-dateutil.
How to choose:
@@ -1,4 +1,4 @@
PDF, Word, and Excel files open with pypdf, python-docx, and openpyxl, a Python file format library for each. MarkItDown turns all three into Markdown.
pypdf reads PDFs, python-docx Word files, and openpyxl Excel sheets: one Python file format library per format. MarkItDown turns any of them into Markdown.
How to choose:
@@ -1,4 +1,4 @@
A 2D game needs a loop: write your own with pygame-ce, the Python game development library, or let Arcade run it. Panda3D does 3D, and Ren'Py visual novels.
pygame-ce leaves the 2D game loop to you, while Arcade runs it for you. For 3D, Panda3D covers Python game development, and Ren'Py builds visual novels.
How to choose:
@@ -1,4 +1,4 @@
If your templates should hold no Python code, Jinja fits: a Python template engine whose sandbox also renders untrusted templates. Mako embeds plain Python.
Whether templates may hold Python code splits the Python template engines: Jinja keeps code out and sandboxes untrusted templates, Mako embeds plain Python.
How to choose:
+1 -1
View File
@@ -1,4 +1,4 @@
For a Django project, pick Django REST framework or Django Ninja. Outside Django, build on FastAPI, a Python API framework based on type hints.
Outside Django, declare each FastAPI endpoint with type hints. Django projects add a Python API framework on top instead: Django REST framework or Django Ninja.
How to choose:
@@ -1,4 +1,4 @@
When you want an ORM, an admin, and auth built in, use Django. Flask, a smaller Python web framework, gives you a core you extend.
How much should a Python web framework build in? Django comes with an ORM, an admin, and auth; Flask gives you a small core to extend.
How to choose:
+1 -1
View File
@@ -1,4 +1,4 @@
In production, Gunicorn serves WSGI apps like Django, and Uvicorn serves ASGI apps like FastAPI. Waitress, a pure-Python web server, runs WSGI apps on Windows.
In production, Gunicorn serves WSGI apps like Django, and Uvicorn ASGI apps like FastAPI. Need a Python web server on Windows? Waitress runs WSGI there.
How to choose:
+1 -1
View File
@@ -1,4 +1,4 @@
With Django, Channels is the pick; with Flask and Socket.IO clients, Flask-SocketIO. Standalone apps run on websockets, a Python WebSocket library.
Your Python WebSocket library follows your framework: Channels on Django, Flask-SocketIO on Flask with Socket.IO clients. websockets needs no framework.
How to choose:
+1 -6
View File
@@ -84,12 +84,7 @@ def collect_names(readme_text: str) -> list[str]:
def fetch_clickpy(names: list[str]) -> dict[str, int]:
in_list = ", ".join(f"'{name}'" for name in names)
query = (
"SELECT project, sum(count) AS downloads "
"FROM pypi.pypi_downloads_per_day "
f"WHERE project IN ({in_list}) AND date >= today() - 30 "
"GROUP BY project FORMAT JSON"
)
query = f"SELECT project, sum(count) AS downloads FROM pypi.pypi_downloads_per_day WHERE project IN ({in_list}) AND date >= today() - 30 GROUP BY project FORMAT JSON"
resp = httpx.post(CLICKPY_URL, content=query, timeout=60)
resp.raise_for_status()
return {row["project"]: int(row["downloads"]) for row in resp.json()["data"]}
+3 -1
View File
@@ -145,7 +145,9 @@ function applyFilters() {
collapseAll();
rows.forEach(function (row) {
// Number rows in their sorted DOM order, not the load order `rows` keeps
const orderedRows = tbody ? tbody.querySelectorAll("tr.row") : rows;
orderedRows.forEach(function (row) {
let show = true;
if (query) {
+1 -1
View File
@@ -241,7 +241,7 @@
<td class="col-arrow"><span class="arrow">&rarr;</span></td>
</tr>
{% if entry.description %}
<tr class="desc-row" aria-hidden="true" hidden>
<tr class="desc-row" hidden>
<td class="col-num"></td>
<td colspan="6">
<div class="desc-text">{{ entry.description | safe }}</div>
+33
View File
@@ -679,6 +679,39 @@ class TestBuild:
{"@type": "ListItem", "position": 2, "name": "Widgets", "item": "https://awesome-python.com/categories/widgets/"},
]
def test_category_json_ld_follows_page_order(self, tmp_path):
readme = textwrap.dedent("""\
# Awesome Python
Intro.
## Projects
**Tools**
## Widgets
_Widget libraries._
- [zeta](https://example.com/zeta) - Listed first.
- [alpha](https://example.com/alpha) - Listed second.
# Contributing
Help!
""")
(tmp_path / "README.md").write_text(readme, encoding="utf-8")
self._copy_real_templates(tmp_path)
build(tmp_path)
category_html = (tmp_path / "website" / "output" / "categories" / "widgets" / "index.html").read_text(encoding="utf-8")
marker = '<script type="application/ld+json">'
start = category_html.index(marker) + len(marker)
data = json.loads(category_html[start : category_html.index("</script>", start)])
collection = next(node for node in data["@graph"] if node["@type"] == "CollectionPage")
items = collection["mainEntity"]["itemListElement"]
assert [(item["position"], item["name"]) for item in items] == [(1, "zeta"), (2, "alpha")]
def test_group_page_falls_back_to_default_description_in_json_ld(self, tmp_path):
readme = textwrap.dedent("""\
# T