refactor(pm): own group-only builds and finish dependency hints
This commit is contained in:
@@ -448,8 +448,8 @@ def _install_provider_deps(llm_id: str, embedder_id: str, vector_id: str) -> Non
|
||||
missing.append(dep)
|
||||
if missing:
|
||||
print("\n The selected backends need extra packages:")
|
||||
print(f" uv pip install {' '.join(missing)}")
|
||||
print(" Run that inside the hermes venv, then re-run setup.")
|
||||
print(f" Missing: {', '.join(missing)}")
|
||||
print(" Declare these requirements in the plugin's pyproject.toml, then run `hermes pm install` and restart Hermes.")
|
||||
|
||||
|
||||
def _probe(fn, ok: str, fail: str, exc=Exception) -> tuple[bool, str]:
|
||||
@@ -510,7 +510,7 @@ def post_setup(hermes_home: str, config: dict) -> None:
|
||||
import mem0
|
||||
installed_ver = getattr(mem0, "__version__", None)
|
||||
if installed_ver and tuple(int(x) for x in installed_ver.split(".")[:3]) < (2, 0, 7):
|
||||
print(f"\n ⚠ mem0ai {installed_ver} installed but >=2.0.7 required.\n Run: uv pip install --python {sys.executable} 'mem0ai>=2.0.7'")
|
||||
print(f"\n ⚠ mem0ai {installed_ver} installed but >=2.0.7 required.\n Run `hermes pm repair`, then restart Hermes.")
|
||||
flags = parse_flags(sys.argv[1:])
|
||||
handler = _MODE_HANDLERS.get(flags["mode"])
|
||||
flags["_mode_from_flag"] = handler is not None
|
||||
|
||||
@@ -787,19 +787,8 @@ def interactive_setup() -> None:
|
||||
|
||||
|
||||
def _install_hint() -> str:
|
||||
"""Build the Teams install hint string.
|
||||
|
||||
Prefers ``uv sync --frozen --extra teams`` (respects pyproject pinning);
|
||||
falls back to a plain pip install of the two packages if that is
|
||||
unavailable. Restarting the gateway also auto-installs via pm.
|
||||
"""
|
||||
try:
|
||||
cmd = "uv sync --frozen --extra teams"
|
||||
except Exception: # pragma: no cover — defensive
|
||||
cmd = None
|
||||
if not cmd:
|
||||
cmd = f"{sys.executable} -m pip install microsoft-teams-apps aiohttp"
|
||||
return f"Teams SDK missing — restart the gateway to auto-install, or run: {cmd}"
|
||||
"""Point to the setup flow that requests PM's declared Teams extra."""
|
||||
return "Teams SDK missing — run `hermes setup`, configure Teams, then restart the gateway"
|
||||
|
||||
|
||||
def register(ctx) -> None:
|
||||
|
||||
@@ -17,6 +17,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser.add_argument("--cache", type=Path)
|
||||
parser.add_argument("--extra", dest="extras", action="append", default=[])
|
||||
parser.add_argument("--group", dest="groups", action="append", default=[])
|
||||
parser.add_argument("--only-groups", action="store_true", help="install selected groups without the application")
|
||||
parser.add_argument("--all-extras", action="store_true")
|
||||
parser.add_argument("--no-install-project", action="store_true")
|
||||
parser.add_argument("--resolve", action="store_true", help="resolve the source lock before building")
|
||||
@@ -77,7 +78,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
else:
|
||||
executable = pm.build_environment(
|
||||
source=args.source, out=args.out, python=args.python, cache=args.cache,
|
||||
extras=args.extras, groups=args.groups, all_extras=args.all_extras,
|
||||
extras=args.extras, groups=args.groups, only_groups=args.only_groups, all_extras=args.all_extras,
|
||||
no_install_project=args.no_install_project, frozen=not args.resolve,
|
||||
sealed=args.sealed, offline=args.offline, explicit=True,
|
||||
)
|
||||
|
||||
@@ -237,7 +237,7 @@ def _python_operation(operation: str, arguments: dict):
|
||||
def build_environment(
|
||||
*, source: Path, out: Path, python: Path | None = None,
|
||||
cache: Path | None = None, env: Mapping[str, str] | None = None,
|
||||
extras: Sequence[str] = (), groups: Sequence[str] = (),
|
||||
extras: Sequence[str] = (), groups: Sequence[str] = (), only_groups: bool = False,
|
||||
all_extras: bool = False, no_install_project: bool = False,
|
||||
frozen: bool = True, sealed: bool = False, offline: bool = False,
|
||||
explicit: bool = False, timeout: int = 1800,
|
||||
@@ -245,7 +245,7 @@ def build_environment(
|
||||
"""Build a validated Python environment without exposing install machinery."""
|
||||
return Path(_python_operation("build_environment", {
|
||||
"source": Path(source), "out": Path(out), "python": python, "cache": cache,
|
||||
"env": dict(env) if env is not None else None, "extras": list(extras), "groups": list(groups),
|
||||
"env": dict(env) if env is not None else None, "extras": list(extras), "groups": list(groups), "only_groups": only_groups,
|
||||
"all_extras": all_extras, "no_install_project": no_install_project,
|
||||
"frozen": frozen, "sealed": sealed, "offline": offline,
|
||||
"explicit": explicit, "timeout": timeout,
|
||||
|
||||
@@ -188,7 +188,7 @@ class PythonEnvironment:
|
||||
def sync(self, source: Path, *, extras: Sequence[str] = (), groups: Sequence[str] = (),
|
||||
timeout: int = 1800, frozen: bool = True, all_extras: bool = False,
|
||||
no_install_project: bool = False, locked: bool = False,
|
||||
no_default_groups: bool = False) -> None:
|
||||
no_default_groups: bool = False, only_groups: bool = False) -> None:
|
||||
"""Install the root and every member; resolve only in a writable workspace.
|
||||
|
||||
``frozen=False`` is reserved for the caller-owned generated workspace,
|
||||
@@ -196,6 +196,8 @@ class PythonEnvironment:
|
||||
"""
|
||||
from pm.workspace import classify_uv_failure
|
||||
|
||||
if only_groups and (not groups or extras or all_extras):
|
||||
raise ValueError("group-only builds require groups and cannot select extras")
|
||||
if not frozen:
|
||||
self.lock(source, timeout=timeout)
|
||||
# Locking members alone is insufficient: plain sync only installs root deps.
|
||||
@@ -216,7 +218,7 @@ class PythonEnvironment:
|
||||
for extra in sorted(set(extras)):
|
||||
command += ["--extra", extra]
|
||||
for group in sorted(set(groups)):
|
||||
command += ["--group", group]
|
||||
command += ["--only-group" if only_groups else "--group", group]
|
||||
result = self._run(command, cwd=source, timeout=timeout)
|
||||
if result.returncode:
|
||||
raise classify_uv_failure("sync", result.returncode, result.stderr or result.stdout)
|
||||
|
||||
@@ -28,7 +28,7 @@ def _require_install_allowed(explicit: bool) -> None:
|
||||
def build_environment(
|
||||
*, source: Path, out: Path, python: Path | None = None,
|
||||
cache: Path | None = None, env: Mapping[str, str] | None = None,
|
||||
extras: Sequence[str] = (), groups: Sequence[str] = (),
|
||||
extras: Sequence[str] = (), groups: Sequence[str] = (), only_groups: bool = False,
|
||||
all_extras: bool = False, no_install_project: bool = False,
|
||||
frozen: bool = True, sealed: bool = False, offline: bool = False,
|
||||
explicit: bool = False, timeout: int = 1800,
|
||||
@@ -57,7 +57,7 @@ def build_environment(
|
||||
out.mkdir(parents=True)
|
||||
try:
|
||||
environment.create()
|
||||
environment.sync(source, extras=extras, groups=groups, all_extras=all_extras,
|
||||
environment.sync(source, extras=extras, groups=groups, only_groups=only_groups, all_extras=all_extras,
|
||||
no_install_project=no_install_project, frozen=frozen, timeout=timeout)
|
||||
environment.check()
|
||||
if sealed:
|
||||
|
||||
2
setup.py
2
setup.py
@@ -39,7 +39,7 @@ _BLOCK_MESSAGE = (
|
||||
"See: https://hermes-agent.nousresearch.com/docs/getting-started/installation\n"
|
||||
"\n"
|
||||
"If you are developing, use an editable install instead:\n"
|
||||
" uv sync # or: uv pip install -e .\n"
|
||||
" source ./activate # PowerShell: . .\\activate.ps1\n"
|
||||
"\n"
|
||||
"If you are building with Nix (uv2nix), this error should not fire —\n"
|
||||
"the Hermes Nix derivation sets HERMES_NIX_BUILD=1. If it does, file a bug."
|
||||
|
||||
@@ -199,6 +199,22 @@ def test_public_dependency_only_build_needs_no_application_source(installable_pr
|
||||
assert not Path(env["HERMES_HOME"]).exists()
|
||||
|
||||
|
||||
def test_group_only_build_excludes_application_dependencies(locked_project, tmp_path):
|
||||
import pm
|
||||
|
||||
source, _, env = locked_project
|
||||
metadata = source / "pyproject.toml"
|
||||
metadata.write_text(metadata.read_text() + '\n[dependency-groups]\nicons=["chosen-dep==1.0"]\n')
|
||||
pm.lock_project(source, python=Path(sys.executable), cache=tmp_path / "cache", env=env,
|
||||
offline=True, explicit=True)
|
||||
python = pm.build_environment(source=source, out=tmp_path / "icons", groups=["icons"],
|
||||
only_groups=True, python=Path(sys.executable), cache=tmp_path / "cache",
|
||||
env=env, offline=True, explicit=True)
|
||||
assert _run([str(python), "-I", "-c", "import chosen_dep, importlib.util; "
|
||||
"assert importlib.util.find_spec('base_dep') is None; print(chosen_dep.__version__)"],
|
||||
cwd=tmp_path, env=env) == "1.0"
|
||||
|
||||
|
||||
def test_child_output_is_live_and_keeps_explicit_index_credentials(tmp_path):
|
||||
import io
|
||||
from pm.environment import PythonEnvironment
|
||||
|
||||
@@ -356,6 +356,7 @@ Use the public `pm` module for Python dependency work:
|
||||
| `pm.sync_venv(extras, explicit=True)` | Prepare and select the complete application dependency union, including enabled plugins. |
|
||||
| `pm.sync_venv(repair=True, explicit=True)` | Replay the recorded dependency set in a new application generation. |
|
||||
| `pm.build_environment(source=..., out=..., explicit=True)` | Build and validate a fresh caller-owned output. No plugin discovery or application selection. |
|
||||
| `pm.build_environment(source=..., out=..., groups=[...], only_groups=True, explicit=True)` | Build only the selected locked dependency groups, without application dependencies. Used by icon builds. |
|
||||
| `pm.lock_project(source, explicit=True)` | Refresh an explicit project's lock without selecting an environment. |
|
||||
| `pm.ensure_environment(name, requirements, explicit=True)` | Prepare and select an isolated dependency generation. Return its Python path. |
|
||||
| `pm.ensure_python_tool(name, requirements, executable, explicit=True)` | Prepare an isolated tool and return its executable path. |
|
||||
|
||||
Reference in New Issue
Block a user