From b5421f4ba606df706472ac1a4b36a84d9e1973c8 Mon Sep 17 00:00:00 2001 From: "Brian D. Evans" Date: Tue, 9 Jun 2026 16:11:48 +0100 Subject: [PATCH] fix(deps): declare packaging as a core dependency so it ships everywhere (#40522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): declare packaging as a core dependency so it ships everywhere packaging is imported directly on three production paths but was never declared in [project.dependencies], so it only reached users transitively (pip/uv pull it for other tools). The slim official Docker image ships without it, where each try/except-ImportError fallback silently degrades: - plugins/memory/hindsight/__init__.py (_meets_minimum_version) returns False when packaging is absent, disabling update_mode='append' so every session leaks separate Hindsight documents (the reported #40503 symptom). - tools/lazy_deps.py (_is_satisfied) falls back to "installed counts as satisfied", defeating every version-constraint check on lazy extras. - hermes_cli/main.py drops to naive name==version requirement parsing. Promote it to a declared core dep pinned to packaging==26.0 — the exact version already resolved in uv.lock, so there is zero resolution churn (the lock change is two edge annotations marking it transitive->direct). It is a pure-Python py3-none-any wheel with no compiled extensions, safe to ship on every platform. Declaring it also wires it into the _verify_core_dependencies_installed() update-repair guard, which reinstalls missing [project.dependencies] on hermes update. Adds a hermetic tomllib-parse regression test that fails before the declaration and passes after. Fixes #40503 * test(deps): make packaging dep-name extraction PEP 508-robust Address Copilot review on #40522: the inline name-extraction only handled ==, >=, [ and ; and could mis-parse valid requirement strings using <=, ~=, !=, <, > or a direct reference (name @ url). Factor a _distribution_name helper that drops markers, direct-reference URLs and extras, then strips any version operator via regex, so a future dep declared with any PEP 508 specifier shape is matched correctly. --------- Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> --- pyproject.toml | 10 +++++++++ tests/test_packaging_metadata.py | 38 ++++++++++++++++++++++++++++++++ uv.lock | 2 ++ 3 files changed, 50 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 54a54da0409..73d0fba4965 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,16 @@ dependencies = [ "prompt_toolkit==3.0.52", # Cron scheduler (built-in feature — scheduled cron/interval jobs use croniter). "croniter==6.0.0", + # ``packaging`` is imported directly on three production paths but was never + # declared, so it only reached users transitively (pip/uv pull it for other + # tools). The slim official Docker image ships without it, where the + # try/except-ImportError fallbacks silently degrade: Hindsight's + # ``_meets_minimum_version`` disables update_mode='append' (#40503), + # tools/lazy_deps.py treats every version constraint as satisfied, and + # hermes_cli/main.py drops to naive requirement parsing. Pure-Python + # py3-none-any wheel, no compiled extensions — safe to ship everywhere. + # Pinned to the version already resolved in uv.lock (no resolution churn). + "packaging==26.0", # Markdown -> HTML conversion for rich message delivery (Matrix # `formatted_body`, and the `send_message` tool's HTML path). Now on the # DEFAULT delivery path, not matrix-specific: without it both diff --git a/tests/test_packaging_metadata.py b/tests/test_packaging_metadata.py index b4c367b142d..edc1cb6d1b3 100644 --- a/tests/test_packaging_metadata.py +++ b/tests/test_packaging_metadata.py @@ -1,4 +1,5 @@ from pathlib import Path +import re import tomllib import pytest @@ -14,6 +15,22 @@ find_packages = pytest.importorskip("setuptools", exc_type=ImportError).find_pac REPO_ROOT = Path(__file__).resolve().parents[1] +def _distribution_name(requirement: str) -> str: + """Extract the PEP 508 distribution name from a requirement string. + + Robust to markers (``; python_version < '3.12'``), direct references + (``name @ https://...``), extras (``name[extra]``) and every version + operator (``==``, ``>=``, ``<=``, ``~=``, ``!=``, ``<``, ``>``), so a + future dep declared with any valid specifier shape doesn't silently + mis-parse here. + """ + spec = requirement.split(";", 1)[0] # drop environment markers + spec = spec.split("@", 1)[0] # drop direct-reference URLs + spec = spec.split("[", 1)[0] # drop extras + spec = re.split(r"[=<>!~]", spec, maxsplit=1)[0] # drop any version operator + return spec.strip().lower() + + def _packages_find_include(): data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) return data["tool"]["setuptools"]["packages"]["find"]["include"] @@ -61,6 +78,27 @@ def test_every_on_disk_subpackage_is_covered_by_packages_find(): ) +def test_packaging_declared_as_core_dependency(): + """Regression for #40503. + + ``packaging`` is imported directly on three production paths + (plugins/memory/hindsight/__init__.py, tools/lazy_deps.py, + hermes_cli/main.py) yet was undeclared, so it only reached users + transitively. The slim Docker image shipped without it, silently + disabling Hindsight append-mode and version-constraint checks. It must + be a declared core dependency so it installs everywhere and the + update-repair step (``_verify_core_dependencies_installed``) guards it. + """ + data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + core = data["project"]["dependencies"] + names = {_distribution_name(dep) for dep in core} + assert "packaging" in names, ( + "packaging is imported on production paths (hindsight version compare, " + "lazy_deps version constraints, requirement parsing) and must be a " + "declared core dependency, not a transitive — see #40503" + ) + + def test_faster_whisper_is_not_a_base_dependency(): data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) deps = data["project"]["dependencies"] diff --git a/uv.lock b/uv.lock index e7d487bf636..bb8718d9b50 100644 --- a/uv.lock +++ b/uv.lock @@ -1400,6 +1400,7 @@ dependencies = [ { name = "jinja2" }, { name = "markdown" }, { name = "openai" }, + { name = "packaging" }, { name = "pathspec" }, { name = "pillow" }, { name = "prompt-toolkit" }, @@ -1650,6 +1651,7 @@ requires-dist = [ { name = "nemo-relay", marker = "extra == 'nemo-relay'", specifier = "==0.3" }, { name = "numpy", marker = "extra == 'voice'", specifier = "==2.4.3" }, { name = "openai", specifier = "==2.24.0" }, + { name = "packaging", specifier = "==26.0" }, { name = "parallel-web", marker = "extra == 'parallel-web'", specifier = "==0.4.2" }, { name = "pathspec", specifier = "==1.1.1" }, { name = "pillow", specifier = "==12.2.0" },