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)
|
missing.append(dep)
|
||||||
if missing:
|
if missing:
|
||||||
print("\n The selected backends need extra packages:")
|
print("\n The selected backends need extra packages:")
|
||||||
print(f" uv pip install {' '.join(missing)}")
|
print(f" Missing: {', '.join(missing)}")
|
||||||
print(" Run that inside the hermes venv, then re-run setup.")
|
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]:
|
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
|
import mem0
|
||||||
installed_ver = getattr(mem0, "__version__", None)
|
installed_ver = getattr(mem0, "__version__", None)
|
||||||
if installed_ver and tuple(int(x) for x in installed_ver.split(".")[:3]) < (2, 0, 7):
|
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:])
|
flags = parse_flags(sys.argv[1:])
|
||||||
handler = _MODE_HANDLERS.get(flags["mode"])
|
handler = _MODE_HANDLERS.get(flags["mode"])
|
||||||
flags["_mode_from_flag"] = handler is not None
|
flags["_mode_from_flag"] = handler is not None
|
||||||
|
|||||||
@@ -787,19 +787,8 @@ def interactive_setup() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _install_hint() -> str:
|
def _install_hint() -> str:
|
||||||
"""Build the Teams install hint string.
|
"""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"
|
||||||
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}"
|
|
||||||
|
|
||||||
|
|
||||||
def register(ctx) -> None:
|
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("--cache", type=Path)
|
||||||
parser.add_argument("--extra", dest="extras", action="append", default=[])
|
parser.add_argument("--extra", dest="extras", action="append", default=[])
|
||||||
parser.add_argument("--group", dest="groups", 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("--all-extras", action="store_true")
|
||||||
parser.add_argument("--no-install-project", 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")
|
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:
|
else:
|
||||||
executable = pm.build_environment(
|
executable = pm.build_environment(
|
||||||
source=args.source, out=args.out, python=args.python, cache=args.cache,
|
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,
|
no_install_project=args.no_install_project, frozen=not args.resolve,
|
||||||
sealed=args.sealed, offline=args.offline, explicit=True,
|
sealed=args.sealed, offline=args.offline, explicit=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ def _python_operation(operation: str, arguments: dict):
|
|||||||
def build_environment(
|
def build_environment(
|
||||||
*, source: Path, out: Path, python: Path | None = None,
|
*, source: Path, out: Path, python: Path | None = None,
|
||||||
cache: Path | None = None, env: Mapping[str, str] | 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,
|
all_extras: bool = False, no_install_project: bool = False,
|
||||||
frozen: bool = True, sealed: bool = False, offline: bool = False,
|
frozen: bool = True, sealed: bool = False, offline: bool = False,
|
||||||
explicit: bool = False, timeout: int = 1800,
|
explicit: bool = False, timeout: int = 1800,
|
||||||
@@ -245,7 +245,7 @@ def build_environment(
|
|||||||
"""Build a validated Python environment without exposing install machinery."""
|
"""Build a validated Python environment without exposing install machinery."""
|
||||||
return Path(_python_operation("build_environment", {
|
return Path(_python_operation("build_environment", {
|
||||||
"source": Path(source), "out": Path(out), "python": python, "cache": cache,
|
"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,
|
"all_extras": all_extras, "no_install_project": no_install_project,
|
||||||
"frozen": frozen, "sealed": sealed, "offline": offline,
|
"frozen": frozen, "sealed": sealed, "offline": offline,
|
||||||
"explicit": explicit, "timeout": timeout,
|
"explicit": explicit, "timeout": timeout,
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ class PythonEnvironment:
|
|||||||
def sync(self, source: Path, *, extras: Sequence[str] = (), groups: Sequence[str] = (),
|
def sync(self, source: Path, *, extras: Sequence[str] = (), groups: Sequence[str] = (),
|
||||||
timeout: int = 1800, frozen: bool = True, all_extras: bool = False,
|
timeout: int = 1800, frozen: bool = True, all_extras: bool = False,
|
||||||
no_install_project: bool = False, locked: 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.
|
"""Install the root and every member; resolve only in a writable workspace.
|
||||||
|
|
||||||
``frozen=False`` is reserved for the caller-owned generated workspace,
|
``frozen=False`` is reserved for the caller-owned generated workspace,
|
||||||
@@ -196,6 +196,8 @@ class PythonEnvironment:
|
|||||||
"""
|
"""
|
||||||
from pm.workspace import classify_uv_failure
|
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:
|
if not frozen:
|
||||||
self.lock(source, timeout=timeout)
|
self.lock(source, timeout=timeout)
|
||||||
# Locking members alone is insufficient: plain sync only installs root deps.
|
# Locking members alone is insufficient: plain sync only installs root deps.
|
||||||
@@ -216,7 +218,7 @@ class PythonEnvironment:
|
|||||||
for extra in sorted(set(extras)):
|
for extra in sorted(set(extras)):
|
||||||
command += ["--extra", extra]
|
command += ["--extra", extra]
|
||||||
for group in sorted(set(groups)):
|
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)
|
result = self._run(command, cwd=source, timeout=timeout)
|
||||||
if result.returncode:
|
if result.returncode:
|
||||||
raise classify_uv_failure("sync", result.returncode, result.stderr or result.stdout)
|
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(
|
def build_environment(
|
||||||
*, source: Path, out: Path, python: Path | None = None,
|
*, source: Path, out: Path, python: Path | None = None,
|
||||||
cache: Path | None = None, env: Mapping[str, str] | 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,
|
all_extras: bool = False, no_install_project: bool = False,
|
||||||
frozen: bool = True, sealed: bool = False, offline: bool = False,
|
frozen: bool = True, sealed: bool = False, offline: bool = False,
|
||||||
explicit: bool = False, timeout: int = 1800,
|
explicit: bool = False, timeout: int = 1800,
|
||||||
@@ -57,7 +57,7 @@ def build_environment(
|
|||||||
out.mkdir(parents=True)
|
out.mkdir(parents=True)
|
||||||
try:
|
try:
|
||||||
environment.create()
|
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)
|
no_install_project=no_install_project, frozen=frozen, timeout=timeout)
|
||||||
environment.check()
|
environment.check()
|
||||||
if sealed:
|
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"
|
"See: https://hermes-agent.nousresearch.com/docs/getting-started/installation\n"
|
||||||
"\n"
|
"\n"
|
||||||
"If you are developing, use an editable install instead:\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"
|
"\n"
|
||||||
"If you are building with Nix (uv2nix), this error should not fire —\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."
|
"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()
|
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):
|
def test_child_output_is_live_and_keeps_explicit_index_credentials(tmp_path):
|
||||||
import io
|
import io
|
||||||
from pm.environment import PythonEnvironment
|
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(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.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=..., 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.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_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. |
|
| `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