Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions pm/plugin_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,14 @@ def read_python_declaration(plugin_dir: Path) -> PythonDeclaration:
text = None
if text is not None:
files.append(project)
if "GENERATED by pm" not in text:
document = tomllib.loads(text)
specs = document.get("project", {}).get("dependencies", [])
document = tomllib.loads(text) if "GENERATED by pm" not in text else {}
# A pyproject with no [project] table only configures tools (ruff, pytest);
# the manifest still declares the dependencies.
if "project" in document:
specs = document["project"].get("dependencies", [])
if not isinstance(specs, list) or any(not isinstance(v, str) for v in specs):
raise ValueError(f"invalid project.dependencies: {project}")
requires_python = document.get("project", {}).get("requires-python")
requires_python = document["project"].get("requires-python")
if requires_python is not None and not isinstance(requires_python, str):
raise ValueError(f"invalid project.requires-python: {project}")
return PythonDeclaration(tuple(files), project, tuple(specs), manifest, requires_python)
Expand Down
20 changes: 20 additions & 0 deletions pm/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,25 @@ def _member_key(identity: Path) -> str:
return f"{name}-{digest}"


def _installable_project(document: dict) -> bool:
"""Drop what Hermes never installs from a plugin's pyproject; True when it changed.

Hermes never installs a plugin's extras, and uv syncs a member's default ``dev``
group into Hermes's own environment. Resolving them anyway made a plugin's
pytest/ruff pins conflict with core's.
"""
project = document.get("project", {})
changed = False
if "optional-dependencies" in project and "optional-dependencies" not in project.get("dynamic", []):
del project["optional-dependencies"]
changed = True
for table, key in ((document, "dependency-groups"), (document.get("tool", {}).get("uv", {}), "dev-dependencies")):
if key in table:
del table[key]
changed = True
return changed


def _workspace_member(plugin_dir: Path, root: Path, *, identity: Path) -> Path:
"""Keep workspace members with their generation, not a temporary install clone."""
import json
Expand All @@ -272,6 +291,7 @@ def _workspace_member(plugin_dir: Path, root: Path, *, identity: Path) -> Path:
changed = declaration.install_requirements != declaration.requirements
if changed:
document["project"]["dependencies"] = list(declaration.install_requirements)
changed = _installable_project(document) or changed
for sources in document.get("tool", {}).get("uv", {}).get("sources", {}).values():
for spec in sources if isinstance(sources, list) else [sources]:
if not isinstance(spec, dict) or "path" not in spec:
Expand Down
22 changes: 22 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,28 @@ uvicorn = false
vercel = false
websockets = false
youtube-transcript-api = false
# Catalog plugins floored on a release younger than the 14-day window
# (temporary). Each cutoff is the upload time of the OLDEST release the
# plugin accepts, so exactly that release gets through and anything newer
# still waits. A line is dead once its date is 14 days old: delete it then.
# The lasting fix is upstream (exact pin + the plugin's own exemption, see
# the developer guide's "Dependency security policy").
# birkin-mnemosyne 0.4.0: catalog birkin-mnemosyne; dead after 2026-10-14
birkin-mnemosyne = "2026-09-30T10:47:28Z"
# cortexlayer 0.1.2: catalog cortexlayer; dead after 2026-10-09
cortexlayer = "2026-09-25T14:28:59Z"
# evalroute 0.9.0: catalog evalroute; dead after 2026-10-19
evalroute = "2026-10-05T04:51:25Z"
# gonogo-eval 0.3.0: catalog gonogo, thomas; dead after 2026-10-12
gonogo-eval = "2026-09-28T21:07:00Z"
# hermes-mnemostack 1.0.3: catalog mnemostack; dead after 2026-10-11
hermes-mnemostack = "2026-09-27T19:26:09Z"
# loreconvo 0.10.15: catalog loreconvo; dead after 2026-10-18
loreconvo = "2026-10-04T17:35:24Z"
# mnemosyne-hermes 0.7.3: catalog mnemosyne; dead after 2026-10-07
mnemosyne-hermes = "2026-09-23T23:42:51Z"
# mnemosyne-memory 4.0.0b3: catalog mnemosyne; dead after 2026-10-07
mnemosyne-memory = "2026-09-23T23:41:58Z"

[tool.setuptools]
# Root single-file modules are derived by setup.py at build time from the
Expand Down
45 changes: 45 additions & 0 deletions tests/pm/test_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,51 @@ def test_missing_explicit_seed_cannot_silently_resolve_new_versions(layout):
assert not (tmp / "env").exists()


def _plugin_pyproject(tmp: Path, name: str, body: str) -> Path:
directory = tmp / name
directory.mkdir()
(directory / "pyproject.toml").write_text(body, encoding="utf-8")
(directory / "plugin.yaml").write_text(f"name: {name}\n", encoding="utf-8")
return directory


def test_plugin_extras_and_dev_groups_never_constrain_hermes(layout):
"""Hermes installs neither a plugin's extras nor its dev group, so their pins (here an
unsatisfiable one against core's ``base-dep==1.0``) must not make the plugin uninstallable."""
import tomllib

tmp, core, _, _ = layout
plugin = _plugin_pyproject(tmp, "pinned-dev", (
'[project]\nname = "pinned-dev"\nversion = "1"\nrequires-python = ">=3.11"\n'
'dependencies = ["member-dep==1.0"]\n'
'[project.optional-dependencies]\ndev = ["base-dep==9.9"]\n'
'[dependency-groups]\ndev = ["other-dep==9.9"]\n[tool.uv]\npackage = false\n'))
root = tmp / "workspace"
ws.lock_and_sync([plugin], [], root=root, source=core, seed_lock=core / "uv.lock",
environment=managed_environment(tmp / "env"))
locked = {p["name"]: p["version"] for p in tomllib.loads((root / "uv.lock").read_text())["package"]}
assert locked["base-dep"] == "1.0" and locked["member-dep"] == "1.0"


def test_tool_config_pyproject_leaves_the_manifest_in_charge_of_dependencies(layout):
"""A pyproject holding only tool settings (ruff, pytest) is not a package definition:
the plugin's manifest dependencies still install, instead of uv refusing a [project]
table PM had to invent."""
import tomllib

tmp, core, _, _ = layout
plugin = tmp / "lint-only"
plugin.mkdir()
(plugin / "pyproject.toml").write_text("[tool.ruff]\nline-length = 100\n", encoding="utf-8")
(plugin / "plugin.yaml").write_text("name: lint-only\npython_dependencies:\n - member-dep==1.0\n",
encoding="utf-8")
root = tmp / "workspace"
ws.lock_and_sync([plugin], [], root=root, source=core, seed_lock=core / "uv.lock",
environment=managed_environment(tmp / "env"))
locked = {p["name"] for p in tomllib.loads((root / "uv.lock").read_text())["package"]}
assert "member-dep" in locked


class _TimedIndex:
"""A PEP 691 JSON index with per-file ``upload-time``.

Expand Down
174 changes: 91 additions & 83 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading