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
2 changes: 1 addition & 1 deletion docs/products.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ object that a reached object references and no census holds is listed as `missin
|---|---|
| inputs | `objects`: the object table of what it reaches (`nnnotes.objects/1`); the stage's own inputs (master tables, data its parameters name) |
| depends | `link.addresses` (when it has keys or resource paths), the `unity.export` and `sprite.crop` tasks whose results it read |
| artifacts | `<task id>#<path>`: the product's own files; `<task id>#objects`: the object table of the objects its files use (the whole table unless the stage narrows it) |
| artifacts | `<task id>#<path>`: the product's own files; `<task id>#objects`: the object table of the objects its files use (the whole table unless the stage narrows it to some objects, or to some files of an object: a shader's programs for one graphics API) |
| item | one for the subject: `exported`, or `unsupported` with a reason code |

The key holds the object table, so a product is assembled again exactly when a file of an object it reaches
Expand Down
35 changes: 28 additions & 7 deletions src/nnnotes/pipeline/assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,35 @@ def json(self, oid: str, role: str = "json"):

@dataclass
class Assembly:
"""What `assemble` makes: the product's files {path: (bytes, extension, kind)} and the object ids its files use
(None: every object of the table)."""
"""What `assemble` makes: the product's files {path: (bytes, extension, kind)} and what of the object table they
use: `objects` the object ids (None: every object of the table), `roles` {object id: the roles of its files the
product keeps} for an object of which it uses some files only (a shader's programs for one graphics API). The
product's object table, and so its manifest, holds only those."""

files: dict
objects: set | None = None
roles: dict = field(default_factory=dict)


def _prune(tid: str, objects: dict, made: Assembly) -> dict:
"""The entries of an object table an Assembly keeps."""
keep = set(objects) if made.objects is None else set(made.objects)
unknown = sorted((keep | set(made.roles)) - set(objects))
if unknown:
raise ValueError(f"{tid}: objects not in the table: {', '.join(unknown[:5])}")
out = {}
for o, e in objects.items():
if o not in keep:
continue
roles = made.roles.get(o)
if roles is not None:
files = e.get("files") or {}
lacking = sorted(set(roles) - set(files))
if lacking:
raise ValueError(f"{tid}: object {o} has no {', '.join(lacking)} file")
e = dict(e, files={r: f for r, f in files.items() if r in roles})
out[o] = e
return out


class ProductStage(Stage):
Expand Down Expand Up @@ -303,11 +327,8 @@ def run(self, task, depot) -> Output:
raise ValueError(f"{task.id}: a product file cannot be named {PRODUCT_TABLE!r}")
aid = contract.artifact_id(task.id, path)
arts.append(contract.artifact(aid, depot.add(body, ext), prov, {"kind": kind}))
if made.objects is not None:
unknown = sorted(set(made.objects) - set(doc["objects"]))
if unknown:
raise ValueError(f"{task.id}: objects not in the table: {', '.join(unknown[:5])}")
doc = dict(doc, objects={o: e for o, e in doc["objects"].items() if o in made.objects})
if made.objects is not None or made.roles:
doc = dict(doc, objects=_prune(task.id, doc["objects"], made))
data = contract.encode(doc)
aid = contract.artifact_id(task.id, PRODUCT_TABLE)
arts.append(contract.artifact(aid, depot.add(data, "json"), prov, {"kind": TABLE_KIND}))
Expand Down
35 changes: 35 additions & 0 deletions tests/pipeline/test_assemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,41 @@ def assemble(self, task, objects):
execute(t, depot, registry(Wrong({"m/a": "key/a"})), force=True)


class Roles(Toy):
"""A Toy that keeps the `json` file alone of the texture its prefab references."""

ROLES: dict = {oid(10): {"json"}}

def assemble(self, task, objects):
made = super().assemble(task, objects)
return assemble.Assembly(made.files, roles=self.ROLES)


def test_a_product_keeps_some_files_of_an_object(tmp_path):
depot = Depot(tmp_path / "s")
env = env_for(depot, {"key/a": [oid(60)]})
exported(depot, env)
t, doc = product(depot, env, "m/a", {"m/a": "key/a"}, stage=Roles({"m/a": "key/a"}))
table = table_of(depot, doc)
assert set(table["objects"][oid(10)]["files"]) == {"json"} # no image
assert oid(60) in table["objects"] and oid(61) in table["objects"] # the others whole
entries, manifest = manifest_of(depot, t, doc)
assert set(manifest["objects"][oid(10)]["files"]) == {"json"}
assert f"{oid(10)}#image" not in {e["id"] for e in entries}


def test_a_product_cannot_keep_a_file_an_object_lacks(tmp_path):
class Wrong(Roles):
ROLES = {oid(10): {"json", "mesh"}}

depot = Depot(tmp_path / "s")
env = env_for(depot, {"key/a": [oid(60)]})
exported(depot, env)
t = describe(Wrong({"m/a": "key/a"}), "m/a", None, env)
with pytest.raises(ValueError, match="has no mesh file"):
execute(t, depot, registry(Wrong({"m/a": "key/a"})), force=True)


# ---------------------------------------------------------------- the layout
def manifest_of(depot, t, doc):
"""(entries, the manifest) of a product layout for one result."""
Expand Down
Loading