Skip to content

Commit 6ff463f

Browse files
committed
dont crash parsing a manifest with unknown fields
1 parent 6af9256 commit 6ff463f

2 files changed

Lines changed: 31 additions & 4 deletions

File tree

tests/test_manifest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,20 @@ def test_chat_only_helper():
2929
assert m.name == "test-ai"
3030
assert m.public is False
3131
assert any(c.name == "chat" for c in m.capabilities)
32+
33+
34+
def test_from_dict_ignores_unknown_fields():
35+
# A manifest published by a newer schema version carries fields a v0.1
36+
# reader doesn't know. Parsing it must degrade gracefully, not raise.
37+
m = Manifest.from_dict({
38+
"schema_version": "0.2",
39+
"name": "future-ai",
40+
"description": "from a newer hub",
41+
"tags": ["vision", "beta"],
42+
"capabilities": [
43+
{"name": "chat", "description": "chat", "cost_per_token": 0.001},
44+
],
45+
})
46+
assert m.name == "future-ai"
47+
assert m.schema_version == "0.2"
48+
assert m.capabilities[0].name == "chat"

zhub/manifest.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from __future__ import annotations
99

10-
from dataclasses import dataclass, field, asdict
10+
from dataclasses import dataclass, field, asdict, fields
1111
from typing import Any, Optional
1212
import json
1313

@@ -75,9 +75,19 @@ def to_json(self) -> str:
7575

7676
@classmethod
7777
def from_dict(cls, data: dict[str, Any]) -> "Manifest":
78-
caps_raw = data.get("capabilities", [])
79-
caps = [Capability(**c) if isinstance(c, dict) else c for c in caps_raw]
80-
kwargs = {k: v for k, v in data.items() if k != "capabilities"}
78+
# Drop keys we don't know about instead of crashing. schema_version
79+
# exists precisely so a newer hub can add fields; a v0.1 reader parsing
80+
# a v0.2 manifest (or any extra top-level key) must degrade gracefully,
81+
# not raise TypeError. Same for unknown keys on a capability.
82+
cap_fields = {f.name for f in fields(Capability)}
83+
caps = [
84+
Capability(**{k: v for k, v in c.items() if k in cap_fields})
85+
if isinstance(c, dict) else c
86+
for c in data.get("capabilities", [])
87+
]
88+
manifest_fields = {f.name for f in fields(cls)} - {"capabilities"}
89+
kwargs = {k: v for k, v in data.items()
90+
if k in manifest_fields and k != "capabilities"}
8191
return cls(capabilities=caps, **kwargs)
8292

8393
@classmethod

0 commit comments

Comments
 (0)