diff --git a/CONTEXT.md b/CONTEXT.md index 434afbc7..8d489c10 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -12,6 +12,10 @@ A single listed project: `- [name](url) - Description.` The unit that is admitte **Sub-item**: An indented awesome-* also-see link under an Entry (awesome-django under django, awesome-pytest under pytest). Not an Entry: it holds no slot, does not count against the Cap, and rides its parent — re-homed or removed with it. Maintainer-only placement, and only for awesome-* lists: a companion project either earns a full Entry in its proper Use Case or is not listed. +**Bundled Entry**: +An Entry for a feature that ships inside a larger library, framework, or tool rather than as its own PyPI package (django.db.models, geodjango, httpx.URL, uv audit). Marked in README.md by a `(part of X)` description prefix, which the website reads to render a "Bundled" badge where a download count would go. It holds a normal slot and is audited like any other Entry; only its download signal is absent. +_Avoid_: Pointer entry (reform-era name) + **Thematic Group**: A bold group line (e.g. "AI & ML", "Web Development") that clusters Sections in the TOC and the Projects body. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e90db05a..d66b00d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,6 +55,14 @@ Use the **PyPI package name** as the display name so developers can copy it dire - [module](https://docs.python.org/3/library/module.html) - (Python standard library) Description. ``` +### Bundled Feature + +A feature that ships inside a larger library, framework, or tool rather than as its own PyPI package. The `(part of X)` prefix is parsed by the website, which renders a "Bundled" badge in place of a download count, so write it exactly in that form and name the parent as developers say it. + +```markdown +- [name](url) - (part of Parent) Description ending with period. +``` + ### Fork of Another Project ```markdown diff --git a/README.md b/README.md index e056ad84..2243300e 100644 --- a/README.md +++ b/README.md @@ -369,7 +369,7 @@ _Libraries for working with HTTP._ - [httpx2](https://github.com/pydantic/httpx2) - HTTP/1.1 and HTTP/2 client with sync and async APIs, maintained by Pydantic ([httpx](https://github.com/encode/httpx) fork). - URL Manipulation - [yarl](https://github.com/aio-libs/yarl) - Yet another URL library. - - [httpx.URL](https://www.python-httpx.org/api/) - The immutable URL class bundled with HTTPX. + - [httpx.URL](https://www.python-httpx.org/api/) - (part of HTTPX) The immutable URL class. ### Web Scraping @@ -399,7 +399,7 @@ _Libraries that implement Object-Relational Mapping or data mapping techniques._ - Relational Databases - [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) - The Python SQL Toolkit and Object Relational Mapper. - [awesome-sqlalchemy](https://github.com/dahlia/awesome-sqlalchemy) - - [django.db.models](https://github.com/django/django) - The Django [ORM](https://docs.djangoproject.com/en/dev/topics/db/models/). + - [django.db.models](https://github.com/django/django) - (part of Django) The Django [ORM](https://docs.djangoproject.com/en/dev/topics/db/models/). - [peewee](https://github.com/coleifer/peewee) - A small, expressive ORM. - [sqlmodel](https://github.com/fastapi/sqlmodel) - SQLModel is based on Python type annotations, and powered by Pydantic and SQLAlchemy. - NoSQL Databases @@ -531,7 +531,7 @@ _Libraries for geocoding addresses and working with latitudes and longitudes._ - [geopandas](https://github.com/geopandas/geopandas) - Python tools for geographic data (GeoSeries/GeoDataFrame) built on pandas. - [geopy](https://github.com/geopy/geopy) - Python Geocoding Toolbox. - [geojson](https://github.com/jazzband/geojson) - Python bindings and utilities for GeoJSON. -- [geodjango](https://github.com/django/django) - A world-class geographic web framework that is part of [Django](https://docs.djangoproject.com/en/dev/ref/contrib/gis/). +- [geodjango](https://github.com/django/django) - (part of Django) A world-class [geographic web framework](https://docs.djangoproject.com/en/dev/ref/contrib/gis/). ### Science @@ -1118,7 +1118,7 @@ _Frameworks and tools for penetration testing._ _Tools for auditing dependencies against known vulnerabilities._ - [pip-audit](https://github.com/pypa/pip-audit) - Audits Python environments and dependency trees for known vulnerabilities, using the PyPI Advisory Database and OSV. -- [uv audit](https://docs.astral.sh/uv/reference/cli/#uv-audit) - uv's built-in dependency vulnerability and malware scanning backed by OSV. +- [uv audit](https://docs.astral.sh/uv/reference/cli/#uv-audit) - (part of uv) Built-in dependency vulnerability and malware scanning backed by OSV. ### Web Security diff --git a/website/build.py b/website/build.py index 8912281f..e3e45ad9 100644 --- a/website/build.py +++ b/website/build.py @@ -31,6 +31,8 @@ SPONSORSHIP_PATH = "/sponsorship/" SPONSORSHIP_PUBLIC_URL = f"{SITE_URL}sponsorship/" SPONSORSHIP_DESCRIPTION = "Sponsorship for awesome-python: tiers, audience, and how to get your product in front of professional Python developers evaluating tools for production use." +BUNDLED_PREFIX_RE = re.compile(r"^\(part of ") + SOURCE_TYPE_DOMAINS = { "docs.python.org": BUILTIN_FILTER, "gitlab.com": "GitLab", @@ -57,6 +59,7 @@ class TemplateEntry(TypedDict): owner: str | None last_commit_at: str | None source_type: str | None + bundled: bool also_see: list[AlsoSee] @@ -520,6 +523,7 @@ def extract_entries( owner=None, last_commit_at=None, source_type=detect_source_type(entry["url"]), + bundled=bool(BUNDLED_PREFIX_RE.match(entry["description"])), also_see=entry["also_see"], ) seen[key] = existing diff --git a/website/templates/category.html b/website/templates/category.html index 6e8c247c..2eca8665 100644 --- a/website/templates/category.html +++ b/website/templates/category.html @@ -156,10 +156,9 @@ {% if entry.downloads is not none %}{{ "{:,}".format(entry.downloads) }}{% elif entry.source_type == - 'Stdlib' %}Stdlib{% else %}Not on PyPI{% endif %} + 'Stdlib' %}Stdlib{% elif + entry.bundled %}Bundled{% else + %}Not on PyPI{% endif %} {% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{% diff --git a/website/templates/index.html b/website/templates/index.html index 2bbe703a..85eea0f3 100644 --- a/website/templates/index.html +++ b/website/templates/index.html @@ -193,10 +193,9 @@ {% if entry.downloads is not none %}{{ "{:,}".format(entry.downloads) }}{% elif entry.source_type == - 'Stdlib' %}Stdlib{% else %}Not on PyPI{% endif %} + 'Stdlib' %}Stdlib{% elif + entry.bundled %}Bundled{% else + %}Not on PyPI{% endif %} {% if entry.stars is not none %}{{ "{:,}".format(entry.stars) }}{% diff --git a/website/tests/test_build.py b/website/tests/test_build.py index e930b440..15e8d085 100644 --- a/website/tests/test_build.py +++ b/website/tests/test_build.py @@ -446,6 +446,7 @@ class TestBuild: - [My-Lib](https://github.com/org/mylib) - On PyPI. - [no-pypi](https://example.com/none) - Not on PyPI. - [asyncio](https://docs.python.org/3/library/asyncio.html) - Stdlib. + - [my-lib.thing](https://github.com/org/mylib) - (part of My-Lib) A bundled feature. # Contributing @@ -470,6 +471,10 @@ class TestBuild: assert "26,305,454" not in html # Default sort: entries with download counts come first assert html.index("My-Lib") < html.index("no-pypi") + # Each no-download entry gets the badge matching why it has no count + assert html.count('Stdlib') == 1 + assert html.count('Bundled') == 1 + assert html.count('Not on PyPI') == 1 def test_build_fails_when_group_and_category_slug_collide(self, tmp_path): readme = textwrap.dedent("""\ @@ -1020,6 +1025,7 @@ def _template_entry(name: str, stars: int | None, source_type: str | None = None owner=None, last_commit_at=None, source_type=source_type, + bundled=False, also_see=[], )