fix(ci): pkg_inventory parser drops inline comments cleanly
Some checks failed
CI / lint (push) Failing after 33s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Deploy / report (push) Successful in 11s
CI / test (push) Successful in 25m55s
Some checks failed
CI / lint (push) Failing after 33s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Deploy / report (push) Successful in 11s
CI / test (push) Successful in 25m55s
The pyproject dep parser did chained `.strip().strip(",").strip('"')`
which left trailing junk on lines with inline comments. The line:
"pymupdf>=1.24", # AGPL-3.0 — flag if shipping outside …
became the spec `pymupdf>=1.24",` — pip rejected it and the
pkg-supply-chain workflow failed on every push (#400-403).
Replace the four duplicated chained-strip blocks with a single
`_extract_dep_specs` helper that pulls the first quoted string from
each non-comment line. Same call from prod/dev/optional/build-system.
Manifest pypi count: 62 → 66 (parser was previously dropping a few
specs entirely, not just mangling pymupdf).
Add tests/dev/test_pkg_inventory.py with 4 regression tests covering
inline comments, blank/comment lines, single-quoted strings, and a
full pyproject excerpt with the bug pattern.
Closes the noise from #400-403.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,234 @@
|
||||
{
|
||||
"apt": {},
|
||||
"apk": {},
|
||||
"apt": {
|
||||
"infra/images/selkies/Dockerfile.upstream": [
|
||||
"\"./virtualgl32_${VIRTUALGL_VERSION}_amd64.deb\"",
|
||||
"\"./virtualgl_${VIRTUALGL_VERSION}_amd64.deb\"",
|
||||
"./google-chrome-stable.deb",
|
||||
"./heroic_launcher.deb",
|
||||
"./kasmvncserver.deb",
|
||||
"./lutris.deb",
|
||||
"./rustdesk.deb",
|
||||
"./selkies-js-interposer.deb",
|
||||
"./virtualgl_${VIRTUALGL_VERSION}_arm64.deb",
|
||||
"GStreamer",
|
||||
"Operating",
|
||||
"adwaita-icon-theme-full",
|
||||
"aom-tools",
|
||||
"appmenu-gtk3-module",
|
||||
"apt-utils",
|
||||
"ark",
|
||||
"aspell",
|
||||
"aspell-en",
|
||||
"breeze",
|
||||
"breeze-cursor-theme",
|
||||
"breeze-gtk-theme",
|
||||
"breeze-icon-theme",
|
||||
"dbus-user-session",
|
||||
"dbus-x11",
|
||||
"debconf-kde-helper",
|
||||
"dependencies",
|
||||
"desktop-file-utils",
|
||||
"dolphin",
|
||||
"dolphin-plugins",
|
||||
"enchant-2",
|
||||
"fakeroot",
|
||||
"fcitx",
|
||||
"fcitx-frontend-gtk2",
|
||||
"fcitx-frontend-gtk3",
|
||||
"fcitx-frontend-qt5",
|
||||
"fcitx-hangul",
|
||||
"fcitx-libpinyin",
|
||||
"fcitx-m17n",
|
||||
"fcitx-module-dbus",
|
||||
"fcitx-module-kimpanel",
|
||||
"fcitx-module-lua",
|
||||
"fcitx-module-x11",
|
||||
"fcitx-mozc",
|
||||
"fcitx-sayura",
|
||||
"fcitx-tools",
|
||||
"fcitx-unikey",
|
||||
"filelight",
|
||||
"firefox",
|
||||
"frameworkintegration",
|
||||
"fuse",
|
||||
"gir1.2-wp-0.5",
|
||||
"gstreamer1.0-libcamera",
|
||||
"gstreamer1.0-pipewire",
|
||||
"gstreamer1.0-plugins-bad",
|
||||
"gwenview",
|
||||
"haveged",
|
||||
"hunspell",
|
||||
"i965-va-driver-shaders",
|
||||
"i965-va-driver-shaders:i386",
|
||||
"im-config",
|
||||
"intel-gpu-tools",
|
||||
"intel-media-va-driver-non-free",
|
||||
"intel-media-va-driver-non-free:i386",
|
||||
"kcalc",
|
||||
"kcharselect",
|
||||
"kde-baseapps",
|
||||
"kde-config-fcitx",
|
||||
"kde-config-gtk-style",
|
||||
"kde-config-gtk-style-preview",
|
||||
"kde-spectacle",
|
||||
"kdeadmin",
|
||||
"kdeconnect",
|
||||
"kdegraphics-thumbnailers",
|
||||
"kdf",
|
||||
"kdialog",
|
||||
"kfind",
|
||||
"kget",
|
||||
"khotkeys",
|
||||
"kimageformat-plugins",
|
||||
"kinfocenter",
|
||||
"kio",
|
||||
"kio-extras",
|
||||
"kmag",
|
||||
"kmenuedit",
|
||||
"kmix",
|
||||
"kmod",
|
||||
"kmousetool",
|
||||
"kmouth",
|
||||
"ksshaskpass",
|
||||
"ktimer",
|
||||
"kwin-addons",
|
||||
"kwin-x11",
|
||||
"kwrite",
|
||||
"libc6:i386",
|
||||
"libdatetime-perl",
|
||||
"libdbusmenu-glib4",
|
||||
"libdbusmenu-gtk3-4",
|
||||
"libdrm2:i386",
|
||||
"libegl-dev",
|
||||
"libegl1:i386",
|
||||
"libffmpeg-nvenc-dev",
|
||||
"libgail-common",
|
||||
"libgdk-pixbuf2.0-bin",
|
||||
"libgl1:i386",
|
||||
"libgles1:i386",
|
||||
"libgles2:i386",
|
||||
"libglu1:i386",
|
||||
"libglvnd0:i386",
|
||||
"libglx0:i386",
|
||||
"libgstreamer-plugins-bad1.0-dev",
|
||||
"libgtk-3-bin",
|
||||
"libgtk2.0-bin",
|
||||
"libkf5baloowidgets-bin",
|
||||
"libkf5dbusaddons-bin",
|
||||
"libkf5iconthemes-bin",
|
||||
"libkf5kdelibs4support5-bin",
|
||||
"libkf5khtml-bin",
|
||||
"libkf5parts-plugins",
|
||||
"libopengl0:i386",
|
||||
"libopenh264-dev",
|
||||
"libpipewire-0.3-modules",
|
||||
"libpipewire-module-x11-bell",
|
||||
"libqt5multimedia5-plugins",
|
||||
"libreoffice",
|
||||
"libreoffice-kf5",
|
||||
"libreoffice-plasma",
|
||||
"libreoffice-style-breeze",
|
||||
"librsvg2-common",
|
||||
"libsm6:i386",
|
||||
"libspa-0.2-bluetooth",
|
||||
"libspa-0.2-jack",
|
||||
"libspa-0.2-modules",
|
||||
"libva-dev",
|
||||
"libva2:i386",
|
||||
"libvulkan-dev:i386",
|
||||
"libx11-6:i386",
|
||||
"libxau6:i386",
|
||||
"libxcb1:i386",
|
||||
"libxdmcp6:i386",
|
||||
"libxext6:i386",
|
||||
"libxtst6:i386",
|
||||
"libxv1:i386",
|
||||
"locales",
|
||||
"media-player-info",
|
||||
"mesa-utils-extra",
|
||||
"mesa-vulkan-drivers:i386",
|
||||
"meson",
|
||||
"nvtop",
|
||||
"okular",
|
||||
"okular-extra-backends",
|
||||
"packages",
|
||||
"pipewire",
|
||||
"pipewire-alsa",
|
||||
"pipewire-audio-client-libraries",
|
||||
"pipewire-jack",
|
||||
"pipewire-libcamera",
|
||||
"pipewire-locales",
|
||||
"pipewire-v4l2",
|
||||
"pipewire-vulkan",
|
||||
"plasma-browser-integration",
|
||||
"plasma-calendar-addons",
|
||||
"plasma-dataengines-addons",
|
||||
"plasma-desktop",
|
||||
"plasma-discover",
|
||||
"plasma-integration",
|
||||
"plasma-runners-addons",
|
||||
"plasma-widgets-addons",
|
||||
"plasma-workspace",
|
||||
"playonlinux",
|
||||
"print-manager",
|
||||
"q4wine",
|
||||
"qapt-deb-installer",
|
||||
"qml-module-org-kde-qqc2desktopstyle",
|
||||
"qml-module-org-kde-runnermodel",
|
||||
"qml-module-qt-labs-platform",
|
||||
"qml-module-qtgraphicaleffects",
|
||||
"qml-module-qtquick-xmllistmodel",
|
||||
"qt5-gtk-platformtheme",
|
||||
"qt5-image-formats-plugins",
|
||||
"qt5-style-plugins",
|
||||
"qtspeech5-flite-plugin",
|
||||
"qtvirtualkeyboard-plugin",
|
||||
"software-properties-qt",
|
||||
"sonnet-plugins",
|
||||
"ssl-cert",
|
||||
"sudo",
|
||||
"svt-av1",
|
||||
"sweeper",
|
||||
"system",
|
||||
"systemsettings",
|
||||
"transmission-qt",
|
||||
"tzdata",
|
||||
"ubuntu-drivers-common",
|
||||
"udev",
|
||||
"va-driver-all",
|
||||
"va-driver-all:i386",
|
||||
"vdpau-driver-all:i386",
|
||||
"vlc",
|
||||
"vlc-plugin-access-extra",
|
||||
"vlc-plugin-notify",
|
||||
"vlc-plugin-samba",
|
||||
"vlc-plugin-skins2",
|
||||
"vlc-plugin-video-splitter",
|
||||
"vlc-plugin-visualization",
|
||||
"winehq-${WINE_BRANCH}",
|
||||
"wireplumber",
|
||||
"wireplumber-locales",
|
||||
"xcvt",
|
||||
"xdg-user-dirs",
|
||||
"xdg-utils",
|
||||
"xvfb"
|
||||
]
|
||||
},
|
||||
"apk": {
|
||||
"infra/gitea/Dockerfile": [
|
||||
"bash",
|
||||
"build-base",
|
||||
"ca-certificates",
|
||||
"curl",
|
||||
"dumb-init",
|
||||
"gettext",
|
||||
"git",
|
||||
"gnupg",
|
||||
"nodejs",
|
||||
"npm",
|
||||
"openssh-keygen"
|
||||
]
|
||||
},
|
||||
"pypi": {
|
||||
"project_prod": [
|
||||
{
|
||||
@@ -8,6 +236,21 @@
|
||||
"version": ">=2.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pymupdf",
|
||||
"version": ">=1.24",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "python-docx",
|
||||
"version": ">=1.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "xlrd",
|
||||
"version": ">=2.0.2",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pydantic",
|
||||
"version": ">=2.0.0",
|
||||
@@ -65,7 +308,7 @@
|
||||
},
|
||||
{
|
||||
"name": "cryptography",
|
||||
"version": ">=46.0.5",
|
||||
"version": ">=46.0.7",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -108,6 +351,21 @@
|
||||
"version": ">=0.41.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "httpx",
|
||||
"version": ">=0.28.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pydo",
|
||||
"version": ">=0.29.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "resend",
|
||||
"version": ">=2.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "narwhals",
|
||||
"version": ">=2.17.0",
|
||||
@@ -128,6 +386,31 @@
|
||||
"version": ">=2.17.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "narwhals",
|
||||
"version": ">=2.17.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "duckdb",
|
||||
"version": ">=1.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "narwhals",
|
||||
"version": ">=2.17.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "typer",
|
||||
"version": ">=0.24.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "narwhals",
|
||||
"version": ">=2.17.0",
|
||||
@@ -143,6 +426,36 @@
|
||||
"version": ">=2024.1.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "anthropic",
|
||||
"version": ">=0.40.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "httpx",
|
||||
"version": ">=0.28.1",
|
||||
"extras": "socks"
|
||||
},
|
||||
{
|
||||
"name": "pyyaml",
|
||||
"version": ">=6.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pydo",
|
||||
"version": ">=0.29.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pdfminer",
|
||||
"version": ".six>=20221105",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "resend",
|
||||
"version": ">=2.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "opentelemetry-api",
|
||||
"version": ">=1.25.0",
|
||||
@@ -179,8 +492,8 @@
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "databricks-cli",
|
||||
"version": ">=0.18.0",
|
||||
"name": "coverage",
|
||||
"version": ">=7.13.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -188,6 +501,11 @@
|
||||
"version": ">=0.85.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "databricks-bundles",
|
||||
"version": ">=0.295.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "boto3",
|
||||
"version": ">=1.35.0",
|
||||
@@ -226,7 +544,28 @@
|
||||
],
|
||||
"project_dev": [],
|
||||
"notebook": [],
|
||||
"dockerfile_adhoc": []
|
||||
"dockerfile_adhoc": [
|
||||
{
|
||||
"name": "altair",
|
||||
"version": "",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "marimo",
|
||||
"version": "==${marimo_version}",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "numpy",
|
||||
"version": "",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pandas",
|
||||
"version": "",
|
||||
"extras": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"npm": [
|
||||
{
|
||||
@@ -288,5 +627,18 @@
|
||||
"version": "latest"
|
||||
}
|
||||
],
|
||||
"base_images": {}
|
||||
"base_images": {
|
||||
"infra/gitea/Dockerfile": [
|
||||
"docker.io/library/golang:1.25-alpine3.22",
|
||||
"docker.io/library/alpine:3.22"
|
||||
],
|
||||
"infra/images/selkies/Dockerfile.upstream": [
|
||||
"${DISTRIB_IMAGE}:${DISTRIB_RELEASE}"
|
||||
],
|
||||
"infra/marimo/src/docker/Dockerfile": [
|
||||
"python:3.13-slim",
|
||||
"base",
|
||||
"data"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,25 @@ def _is_self_ref(spec: str) -> bool:
|
||||
return name == "stack"
|
||||
|
||||
|
||||
def _extract_dep_specs(block: str) -> list[str]:
|
||||
"""Extract dependency spec strings from a TOML array body.
|
||||
|
||||
Pulls the first quoted string from each non-comment line. This
|
||||
correctly skips inline comments (e.g. ``"pymupdf>=1.24", # AGPL``),
|
||||
trailing commas, and surrounding whitespace — the previous chained
|
||||
``.strip()`` approach broke on inline comments.
|
||||
"""
|
||||
specs: list[str] = []
|
||||
for line in block.splitlines():
|
||||
s = line.strip()
|
||||
if not s or s.startswith("#"):
|
||||
continue
|
||||
m = re.match(r'["\']([^"\']+)["\']', s)
|
||||
if m:
|
||||
specs.append(m.group(1).strip())
|
||||
return specs
|
||||
|
||||
|
||||
def _parse_pyproject_deps(text: str) -> tuple[list[dict], list[dict]]:
|
||||
"""Parse dependencies from pyproject.toml without a TOML library.
|
||||
|
||||
@@ -181,10 +200,9 @@ def _parse_pyproject_deps(text: str) -> tuple[list[dict], list[dict]]:
|
||||
# prod deps — [project] dependencies
|
||||
m = re.search(r"^dependencies\s*=\s*\[(.*?)\]", text, re.MULTILINE | re.DOTALL)
|
||||
if m:
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip().strip(",").strip('"').strip("'")
|
||||
if line and not line.startswith("#") and not _is_self_ref(line):
|
||||
prod.append(_parse_pypi_spec(line))
|
||||
for spec in _extract_dep_specs(m.group(1)):
|
||||
if not _is_self_ref(spec):
|
||||
prod.append(_parse_pypi_spec(spec))
|
||||
|
||||
# optional deps — [project.optional-dependencies] all sections
|
||||
# Extract the section content, then parse each "name = [...]" block.
|
||||
@@ -214,18 +232,16 @@ def _parse_pyproject_deps(text: str) -> tuple[list[dict], list[dict]]:
|
||||
depth -= 1
|
||||
i += 1
|
||||
block = rest[: i - 1] if depth == 0 else rest
|
||||
for line in block.splitlines():
|
||||
line = line.strip().strip(",").strip('"').strip("'")
|
||||
if line and not line.startswith("#") and not _is_self_ref(line):
|
||||
prod.append(_parse_pypi_spec(line))
|
||||
for spec in _extract_dep_specs(block):
|
||||
if not _is_self_ref(spec):
|
||||
prod.append(_parse_pypi_spec(spec))
|
||||
|
||||
# dev deps — [dependency-groups] dev
|
||||
m = re.search(r"dev\s*=\s*\[(.*?)\]", text, re.MULTILINE | re.DOTALL)
|
||||
if m:
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip().strip(",").strip('"').strip("'")
|
||||
if line and not line.startswith("#") and not _is_self_ref(line):
|
||||
dev.append(_parse_pypi_spec(line))
|
||||
for spec in _extract_dep_specs(m.group(1)):
|
||||
if not _is_self_ref(spec):
|
||||
dev.append(_parse_pypi_spec(spec))
|
||||
|
||||
# build-system requires
|
||||
m = re.search(
|
||||
@@ -234,10 +250,9 @@ def _parse_pyproject_deps(text: str) -> tuple[list[dict], list[dict]]:
|
||||
re.MULTILINE | re.DOTALL,
|
||||
)
|
||||
if m:
|
||||
for line in m.group(1).splitlines():
|
||||
line = line.strip().strip(",").strip('"').strip("'")
|
||||
if line and not line.startswith("#") and not _is_self_ref(line):
|
||||
prod.append(_parse_pypi_spec(line))
|
||||
for spec in _extract_dep_specs(m.group(1)):
|
||||
if not _is_self_ref(spec):
|
||||
prod.append(_parse_pypi_spec(spec))
|
||||
|
||||
return prod, dev
|
||||
|
||||
|
||||
0
tests/dev/__init__.py
Normal file
0
tests/dev/__init__.py
Normal file
75
tests/dev/test_pkg_inventory.py
Normal file
75
tests/dev/test_pkg_inventory.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""Regression tests for dev/scripts/pkg_inventory.py.
|
||||
|
||||
Specifically guards the inline-comment bug that caused pkg-supply-chain
|
||||
to fail on every push (issue #400-403): the previous parser did
|
||||
``line.strip().strip(",").strip('"')`` which left trailing junk when a
|
||||
dep line had an inline ``# comment``, producing invalid pip specs like
|
||||
``pymupdf>=1.24",`` that broke the downstream PyPI mirror sync.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Load the script as a module — it lives outside the src/ tree.
|
||||
_SCRIPT = Path(__file__).resolve().parents[2] / "dev" / "scripts" / "pkg_inventory.py"
|
||||
_spec = importlib.util.spec_from_file_location("_pkg_inventory", _SCRIPT)
|
||||
assert _spec and _spec.loader
|
||||
_pkg_inventory = importlib.util.module_from_spec(_spec)
|
||||
sys.modules["_pkg_inventory"] = _pkg_inventory
|
||||
_spec.loader.exec_module(_pkg_inventory)
|
||||
|
||||
|
||||
def test_extract_dep_specs_strips_inline_comments():
|
||||
block = """
|
||||
"pymupdf>=1.24", # AGPL-3.0 — flag if shipping outside internal use
|
||||
"python-docx>=1.1",
|
||||
"ruff>=0.11.0", # linter
|
||||
"no-comment>=2.0",
|
||||
"""
|
||||
specs = _pkg_inventory._extract_dep_specs(block)
|
||||
assert specs == [
|
||||
"pymupdf>=1.24",
|
||||
"python-docx>=1.1",
|
||||
"ruff>=0.11.0",
|
||||
"no-comment>=2.0",
|
||||
]
|
||||
|
||||
|
||||
def test_extract_dep_specs_skips_blank_and_comment_lines():
|
||||
block = """
|
||||
|
||||
# this whole line is a comment
|
||||
"pkg>=1.0",
|
||||
# indented comment
|
||||
"""
|
||||
assert _pkg_inventory._extract_dep_specs(block) == ["pkg>=1.0"]
|
||||
|
||||
|
||||
def test_extract_dep_specs_handles_single_quotes():
|
||||
block = """
|
||||
'singlequoted>=1.0',
|
||||
"doublequoted>=2.0",
|
||||
"""
|
||||
assert _pkg_inventory._extract_dep_specs(block) == [
|
||||
"singlequoted>=1.0",
|
||||
"doublequoted>=2.0",
|
||||
]
|
||||
|
||||
|
||||
def test_parse_pyproject_deps_real_pymupdf_line():
|
||||
"""Full integration — parse a small pyproject excerpt with the bug pattern."""
|
||||
text = """[project]
|
||||
name = "test"
|
||||
dependencies = [
|
||||
"pymupdf>=1.24", # AGPL-3.0 — flag if shipping outside internal use
|
||||
"python-docx>=1.1",
|
||||
]
|
||||
"""
|
||||
prod, _dev = _pkg_inventory._parse_pyproject_deps(text)
|
||||
names = sorted(p["name"] for p in prod)
|
||||
assert names == ["pymupdf", "python-docx"]
|
||||
versions = {p["name"]: p["version"] for p in prod}
|
||||
assert versions["pymupdf"] == ">=1.24" # NOT '>=1.24",'
|
||||
Reference in New Issue
Block a user