weft-usd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- absent/__init__.py +22 -0
- absent/__main__.py +98 -0
- absent/detect.py +357 -0
- deps/__init__.py +19 -0
- deps/__main__.py +358 -0
- deps/graph.py +388 -0
- deps/walk.py +216 -0
- weft/__init__.py +11 -0
- weft/__main__.py +4 -0
- weft/ask.py +334 -0
- weft/chunker.py +221 -0
- weft/cli.py +1362 -0
- weft/compose.py +174 -0
- weft/contracts.py +542 -0
- weft/data.py +259 -0
- weft/drift.py +473 -0
- weft/extract.py +566 -0
- weft/follow.py +66 -0
- weft/generators/__init__.py +240 -0
- weft/generators/vehicle.py +135 -0
- weft/kept.py +135 -0
- weft/kinds.py +400 -0
- weft/ledger.py +693 -0
- weft/loom/__init__.py +11 -0
- weft/loom/app.html +551 -0
- weft/loom/server.py +446 -0
- weft/loom/viewer_template.html +1222 -0
- weft/manifest.py +164 -0
- weft/pull.py +580 -0
- weft/pullcache.py +133 -0
- weft/query.py +245 -0
- weft/reeval.py +220 -0
- weft/registry.py +74 -0
- weft/relkinds.py +447 -0
- weft/slice.py +845 -0
- weft/store.py +330 -0
- weft/threadgraph.py +117 -0
- weft/usda.py +332 -0
- weft_usd-0.1.0.dist-info/METADATA +238 -0
- weft_usd-0.1.0.dist-info/RECORD +45 -0
- weft_usd-0.1.0.dist-info/WHEEL +5 -0
- weft_usd-0.1.0.dist-info/entry_points.txt +3 -0
- weft_usd-0.1.0.dist-info/licenses/LICENSE +201 -0
- weft_usd-0.1.0.dist-info/licenses/NOTICE +7 -0
- weft_usd-0.1.0.dist-info/top_level.txt +3 -0
absent/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""absent -- find what a USD stage refers to that is no longer there.
|
|
2
|
+
|
|
3
|
+
USD can tell you what is in a scene. This tells you what isn't, and why.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .detect import ( # noqa: F401
|
|
7
|
+
ANCHOR_KEY,
|
|
8
|
+
AUTHORED_AGAINST_KEY,
|
|
9
|
+
DEAD,
|
|
10
|
+
MOVED,
|
|
11
|
+
UNKNOWN,
|
|
12
|
+
Finding,
|
|
13
|
+
Report,
|
|
14
|
+
anchor_index,
|
|
15
|
+
scan,
|
|
16
|
+
scan_file,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ANCHOR_KEY", "AUTHORED_AGAINST_KEY", "DEAD", "MOVED", "UNKNOWN",
|
|
21
|
+
"Finding", "Report", "anchor_index", "scan", "scan_file",
|
|
22
|
+
]
|
absent/__main__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""absent -- report what a USD stage refers to that is no longer there.
|
|
2
|
+
|
|
3
|
+
python -m absent shot.usda
|
|
4
|
+
python -m absent shot.usda --json
|
|
5
|
+
python -m absent shot.usda --anchor-key "weft:id"
|
|
6
|
+
|
|
7
|
+
Exits 1 if anything is dead, so it can gate a publish.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from .detect import ANCHOR_KEY, DEAD, MOVED, UNKNOWN, scan_file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _render(report, show_unknown: bool) -> str:
|
|
20
|
+
out = []
|
|
21
|
+
groups = [
|
|
22
|
+
(DEAD, report.dead, "gone -- nothing defines this any more"),
|
|
23
|
+
(MOVED, report.moved, "the anchor still exists, at a new path"),
|
|
24
|
+
]
|
|
25
|
+
if show_unknown:
|
|
26
|
+
groups.append(
|
|
27
|
+
(UNKNOWN, report.unknown,
|
|
28
|
+
"refers to nothing, and nothing was recorded to say why"))
|
|
29
|
+
|
|
30
|
+
for label, items, gloss in groups:
|
|
31
|
+
if not items:
|
|
32
|
+
continue
|
|
33
|
+
out.append("")
|
|
34
|
+
out.append("%s (%d) -- %s" % (label.upper(), len(items), gloss))
|
|
35
|
+
out.append("-" * 72)
|
|
36
|
+
for f in items:
|
|
37
|
+
out.append(str(f))
|
|
38
|
+
|
|
39
|
+
out.append("")
|
|
40
|
+
out.append("-" * 72)
|
|
41
|
+
out.append("scanned %d prims, %d prim specs, %d anchors"
|
|
42
|
+
% (report.prims_scanned, report.specs_scanned, report.anchors_found))
|
|
43
|
+
out.append("dead %d moved %d unknown %d"
|
|
44
|
+
% (len(report.dead), len(report.moved), len(report.unknown)))
|
|
45
|
+
|
|
46
|
+
if report.anchors_found == 0 and (report.unknown or report.dead):
|
|
47
|
+
out.append("")
|
|
48
|
+
out.append("No anchor names on this stage, so every absence reads as "
|
|
49
|
+
"'unknown'.")
|
|
50
|
+
out.append("Anchors let this tool separate 'gone' from 'moved' -- see "
|
|
51
|
+
"--anchor-key.")
|
|
52
|
+
return "\n".join(out)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main(argv=None) -> int:
|
|
56
|
+
ap = argparse.ArgumentParser(
|
|
57
|
+
prog="absent",
|
|
58
|
+
description="Find what a USD stage refers to that is no longer there.")
|
|
59
|
+
ap.add_argument("stage", help="a .usd / .usda / .usdc file")
|
|
60
|
+
ap.add_argument("--anchor-key", default=ANCHOR_KEY,
|
|
61
|
+
help="prim custom-data key holding the stable anchor name "
|
|
62
|
+
"(default: %(default)s)")
|
|
63
|
+
ap.add_argument("--json", action="store_true", dest="as_json",
|
|
64
|
+
help="emit findings as JSON")
|
|
65
|
+
ap.add_argument("--quiet-unknown", action="store_true",
|
|
66
|
+
help="hide findings where nothing was recorded to explain them")
|
|
67
|
+
args = ap.parse_args(argv)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
report = scan_file(args.stage, args.anchor_key)
|
|
71
|
+
except IOError as e:
|
|
72
|
+
print("absent: %s" % e, file=sys.stderr)
|
|
73
|
+
return 2
|
|
74
|
+
|
|
75
|
+
if args.as_json:
|
|
76
|
+
print(json.dumps({
|
|
77
|
+
"stage": args.stage,
|
|
78
|
+
"scanned": {
|
|
79
|
+
"prims": report.prims_scanned,
|
|
80
|
+
"prim_specs": report.specs_scanned,
|
|
81
|
+
"anchors": report.anchors_found,
|
|
82
|
+
},
|
|
83
|
+
"findings": [
|
|
84
|
+
{
|
|
85
|
+
"kind": f.kind, "path": f.path, "detail": f.detail,
|
|
86
|
+
"verdict": f.verdict, "layer": f.layer, "moved_to": f.moved_to,
|
|
87
|
+
}
|
|
88
|
+
for f in report.findings
|
|
89
|
+
],
|
|
90
|
+
}, indent=2))
|
|
91
|
+
else:
|
|
92
|
+
print(_render(report, not args.quiet_unknown))
|
|
93
|
+
|
|
94
|
+
return 1 if report.dead else 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(main())
|
absent/detect.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Find what a USD stage refers to that is no longer there.
|
|
2
|
+
|
|
3
|
+
USD can tell you what is in a scene. It cannot tell you what is missing.
|
|
4
|
+
|
|
5
|
+
An `over` at a path that no longer composes is *structurally identical* to a
|
|
6
|
+
legitimate sparse override authored ahead of its target -- which is why none of
|
|
7
|
+
USD's shipped validators catch one, and why they never will. The same silence
|
|
8
|
+
covers relationship targets that resolve to nothing, material bindings to
|
|
9
|
+
deactivated materials, and collection patterns that match zero prims.
|
|
10
|
+
|
|
11
|
+
This module reports those. On a stock stage it can say "this refers to nothing".
|
|
12
|
+
Given anchor names (see ANCHOR_KEY), it can say *why*: the target is gone, or the
|
|
13
|
+
target moved and here is where it went. That distinction is the whole argument
|
|
14
|
+
for carrying a name separate from the hierarchy.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Dict, Iterable, Iterator, List, Optional
|
|
21
|
+
|
|
22
|
+
from pxr import Sdf, Usd, UsdShade
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# A stable name for a thing a scene refers to, carried in-band as prim custom
|
|
26
|
+
# data. Never an address -- nothing resolves it to fetch a prim on the hot path.
|
|
27
|
+
ANCHOR_KEY = "absent:id"
|
|
28
|
+
|
|
29
|
+
# Recorded by whoever authors an override: the anchor it was authored against.
|
|
30
|
+
# This is what turns "refers to nothing" into "dead" versus "moved".
|
|
31
|
+
AUTHORED_AGAINST_KEY = "absent:authoredAgainst"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEAD = "dead" # target absent, and its anchor is absent too
|
|
35
|
+
MOVED = "moved" # target absent, but its anchor is elsewhere on the stage
|
|
36
|
+
UNKNOWN = "unknown" # target absent, nothing recorded -- cannot tell why
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Finding:
|
|
41
|
+
kind: str # what sort of absence
|
|
42
|
+
path: str # where the dangling reference lives
|
|
43
|
+
detail: str # what it points at
|
|
44
|
+
verdict: str = UNKNOWN
|
|
45
|
+
layer: str = "" # which layer authored it, when known
|
|
46
|
+
moved_to: Optional[str] = None # populated when verdict is MOVED
|
|
47
|
+
|
|
48
|
+
def __str__(self) -> str:
|
|
49
|
+
head = "%-22s %-7s %s" % (self.kind, self.verdict, self.path)
|
|
50
|
+
if self.moved_to:
|
|
51
|
+
return "%s\n %s\n anchor now at %s" % (head, self.detail, self.moved_to)
|
|
52
|
+
return "%s\n %s" % (head, self.detail)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Report:
|
|
57
|
+
findings: List[Finding] = field(default_factory=list)
|
|
58
|
+
prims_scanned: int = 0
|
|
59
|
+
specs_scanned: int = 0
|
|
60
|
+
anchors_found: int = 0
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def dead(self) -> List[Finding]:
|
|
64
|
+
return [f for f in self.findings if f.verdict == DEAD]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def moved(self) -> List[Finding]:
|
|
68
|
+
return [f for f in self.findings if f.verdict == MOVED]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def unknown(self) -> List[Finding]:
|
|
72
|
+
return [f for f in self.findings if f.verdict == UNKNOWN]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# -- anchors ---------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def anchor_index(stage: Usd.Stage, key: str = ANCHOR_KEY) -> Dict[str, str]:
|
|
78
|
+
"""Map every anchor name on the stage to the path it currently sits at.
|
|
79
|
+
|
|
80
|
+
This is the only thing anchor names are used for here: telling a target that
|
|
81
|
+
vanished from a target that moved. They are never used to address anything.
|
|
82
|
+
"""
|
|
83
|
+
index: Dict[str, str] = {}
|
|
84
|
+
for prim in stage.TraverseAll():
|
|
85
|
+
name = prim.GetCustomDataByKey(key)
|
|
86
|
+
if name:
|
|
87
|
+
index[str(name)] = str(prim.GetPath())
|
|
88
|
+
return index
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _nested_get(data, key: str):
|
|
92
|
+
"""Read a colon-delimited key out of a raw customData dict.
|
|
93
|
+
|
|
94
|
+
Usd.Prim.GetCustomDataByKey treats ':' as a nesting delimiter, so a key like
|
|
95
|
+
"absent:id" lives at customData["absent"]["id"]. Sdf spec customData is a
|
|
96
|
+
plain dict, so we have to walk it ourselves to match that behaviour.
|
|
97
|
+
"""
|
|
98
|
+
cur = data
|
|
99
|
+
for part in key.split(":"):
|
|
100
|
+
if not hasattr(cur, "get"):
|
|
101
|
+
return None
|
|
102
|
+
cur = cur.get(part)
|
|
103
|
+
if cur is None:
|
|
104
|
+
return None
|
|
105
|
+
return cur
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _adjudicate(recorded: Optional[str], anchors: Dict[str, str]) -> tuple:
|
|
109
|
+
"""Given the anchor an override recorded, decide dead / moved / unknown."""
|
|
110
|
+
if not recorded:
|
|
111
|
+
return UNKNOWN, None
|
|
112
|
+
where = anchors.get(str(recorded))
|
|
113
|
+
if where:
|
|
114
|
+
return MOVED, where
|
|
115
|
+
return DEAD, None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# -- spec walking ----------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def _walk_prim_specs(spec) -> Iterator:
|
|
121
|
+
yield spec
|
|
122
|
+
for child in spec.nameChildren:
|
|
123
|
+
yield from _walk_prim_specs(child)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _local_layers(stage: Usd.Stage) -> List[Sdf.Layer]:
|
|
127
|
+
"""The layers this pipeline authors into -- the root layer stack.
|
|
128
|
+
|
|
129
|
+
Deliberately excludes referenced and payloaded layers: an `over` inside a
|
|
130
|
+
published asset is that asset's business, not this stage's.
|
|
131
|
+
"""
|
|
132
|
+
session = stage.GetSessionLayer()
|
|
133
|
+
return [ly for ly in stage.GetLayerStack(includeSessionLayers=False) if ly != session]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# -- the checks ------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
def stale_overrides(stage: Usd.Stage, anchors: Dict[str, str], report: Report) -> None:
|
|
139
|
+
"""`over` specs whose composed prim is not defined by anything.
|
|
140
|
+
|
|
141
|
+
The classic silent failure: the asset renamed Hat to Cap, the lighter's
|
|
142
|
+
visibility override stayed behind, and it composes to nothing forever.
|
|
143
|
+
"""
|
|
144
|
+
for layer in _local_layers(stage):
|
|
145
|
+
for root in layer.rootPrims:
|
|
146
|
+
for spec in _walk_prim_specs(root):
|
|
147
|
+
report.specs_scanned += 1
|
|
148
|
+
if spec.specifier != Sdf.SpecifierOver:
|
|
149
|
+
continue
|
|
150
|
+
prim = stage.GetPrimAtPath(spec.path)
|
|
151
|
+
if prim and prim.IsValid() and prim.IsDefined():
|
|
152
|
+
continue # a legitimate sparse override -- says nothing
|
|
153
|
+
recorded = _nested_get(spec.customData, AUTHORED_AGAINST_KEY)
|
|
154
|
+
verdict, moved_to = _adjudicate(recorded, anchors)
|
|
155
|
+
report.findings.append(Finding(
|
|
156
|
+
kind="stale-override",
|
|
157
|
+
path=str(spec.path),
|
|
158
|
+
detail="over composes to nothing; no prim is defined here",
|
|
159
|
+
verdict=verdict,
|
|
160
|
+
layer=layer.identifier,
|
|
161
|
+
moved_to=moved_to,
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def overrides_under_instances(stage: Usd.Stage, report: Report) -> None:
|
|
166
|
+
"""`over` specs beneath an instanceable prim, which USD silently discards."""
|
|
167
|
+
for layer in _local_layers(stage):
|
|
168
|
+
for root in layer.rootPrims:
|
|
169
|
+
for spec in _walk_prim_specs(root):
|
|
170
|
+
if spec.specifier != Sdf.SpecifierOver:
|
|
171
|
+
continue
|
|
172
|
+
prim = stage.GetPrimAtPath(spec.path)
|
|
173
|
+
if not (prim and prim.IsValid()):
|
|
174
|
+
continue
|
|
175
|
+
if not prim.IsInstanceProxy():
|
|
176
|
+
continue
|
|
177
|
+
report.findings.append(Finding(
|
|
178
|
+
kind="override-in-instance",
|
|
179
|
+
path=str(spec.path),
|
|
180
|
+
detail="beneath an instanceable prim; this opinion is discarded",
|
|
181
|
+
verdict=DEAD,
|
|
182
|
+
layer=layer.identifier,
|
|
183
|
+
))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def dangling_targets(stage: Usd.Stage, anchors: Dict[str, str], report: Report) -> None:
|
|
187
|
+
"""Relationship targets that resolve to nothing, or to a deactivated prim."""
|
|
188
|
+
for prim in stage.TraverseAll():
|
|
189
|
+
report.prims_scanned += 1
|
|
190
|
+
for rel in prim.GetRelationships():
|
|
191
|
+
# material:binding gets a more specific check of its own.
|
|
192
|
+
if rel.GetName().startswith("material:binding"):
|
|
193
|
+
continue
|
|
194
|
+
for target in rel.GetTargets():
|
|
195
|
+
prim_path = target.GetPrimPath()
|
|
196
|
+
tgt = stage.GetPrimAtPath(prim_path)
|
|
197
|
+
|
|
198
|
+
if not (tgt and tgt.IsValid()):
|
|
199
|
+
recorded = prim.GetCustomDataByKey(AUTHORED_AGAINST_KEY)
|
|
200
|
+
verdict, moved_to = _adjudicate(recorded, anchors)
|
|
201
|
+
report.findings.append(Finding(
|
|
202
|
+
kind="dangling-target",
|
|
203
|
+
path="%s.%s" % (prim.GetPath(), rel.GetName()),
|
|
204
|
+
detail="targets %s, which does not exist" % target,
|
|
205
|
+
verdict=verdict,
|
|
206
|
+
moved_to=moved_to,
|
|
207
|
+
))
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
if not tgt.IsActive():
|
|
211
|
+
report.findings.append(Finding(
|
|
212
|
+
kind="target-deactivated",
|
|
213
|
+
path="%s.%s" % (prim.GetPath(), rel.GetName()),
|
|
214
|
+
detail="targets %s, which is deactivated -- still resolves, "
|
|
215
|
+
"silently" % target,
|
|
216
|
+
verdict=DEAD,
|
|
217
|
+
))
|
|
218
|
+
elif not tgt.IsDefined():
|
|
219
|
+
report.findings.append(Finding(
|
|
220
|
+
kind="dangling-target",
|
|
221
|
+
path="%s.%s" % (prim.GetPath(), rel.GetName()),
|
|
222
|
+
detail="targets %s, which is override-only and never "
|
|
223
|
+
"defined" % target,
|
|
224
|
+
verdict=UNKNOWN,
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def stripped_targets(stage: Usd.Stage, report: Report) -> None:
|
|
229
|
+
"""Relationships whose targets were authored, then removed by composition.
|
|
230
|
+
|
|
231
|
+
When a relationship target points outside the scope of the reference that
|
|
232
|
+
brought it in, USD discards the target — the relationship survives with an
|
|
233
|
+
empty target list. A mesh whose material binding is emptied this way renders
|
|
234
|
+
unshaded.
|
|
235
|
+
|
|
236
|
+
USD does warn at composition time, but that warning goes to a log during a
|
|
237
|
+
scene open that prints hundreds of lines, and the stage is otherwise valid.
|
|
238
|
+
This finds it after the fact, which is when anyone actually looks.
|
|
239
|
+
"""
|
|
240
|
+
for prim in stage.TraverseAll():
|
|
241
|
+
for rel in prim.GetRelationships():
|
|
242
|
+
if rel.GetTargets():
|
|
243
|
+
continue
|
|
244
|
+
authored = []
|
|
245
|
+
try:
|
|
246
|
+
for spec in rel.GetPropertyStack(Usd.TimeCode.Default()):
|
|
247
|
+
items = spec.targetPathList
|
|
248
|
+
for slot in ("explicitItems", "prependedItems", "appendedItems",
|
|
249
|
+
"addedItems"):
|
|
250
|
+
authored.extend(getattr(items, slot, ()) or ())
|
|
251
|
+
except Exception:
|
|
252
|
+
continue
|
|
253
|
+
if not authored:
|
|
254
|
+
continue # a relationship that never had targets is not a finding
|
|
255
|
+
report.findings.append(Finding(
|
|
256
|
+
kind="targets-stripped",
|
|
257
|
+
path="%s.%s" % (prim.GetPath(), rel.GetName()),
|
|
258
|
+
detail="targets %s were authored, then dropped in composition — "
|
|
259
|
+
"out of scope of the reference that brought them in"
|
|
260
|
+
% ", ".join(str(a) for a in authored[:3]),
|
|
261
|
+
verdict=DEAD,
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def material_bindings(stage: Usd.Stage, report: Report) -> None:
|
|
266
|
+
"""Bindings to a material that is missing or deactivated.
|
|
267
|
+
|
|
268
|
+
ComputeBoundMaterial() hands back a valid UsdShade.Material wrapping an
|
|
269
|
+
inactive prim, so nothing downstream notices until a render looks wrong.
|
|
270
|
+
"""
|
|
271
|
+
for prim in stage.Traverse():
|
|
272
|
+
rel = UsdShade.MaterialBindingAPI(prim).GetDirectBindingRel()
|
|
273
|
+
if not rel:
|
|
274
|
+
continue
|
|
275
|
+
for target in rel.GetTargets():
|
|
276
|
+
mat = stage.GetPrimAtPath(target.GetPrimPath())
|
|
277
|
+
if not (mat and mat.IsValid()):
|
|
278
|
+
report.findings.append(Finding(
|
|
279
|
+
kind="binding-missing",
|
|
280
|
+
path=str(prim.GetPath()),
|
|
281
|
+
detail="bound to %s, which does not exist" % target,
|
|
282
|
+
verdict=DEAD,
|
|
283
|
+
))
|
|
284
|
+
elif not mat.IsActive():
|
|
285
|
+
report.findings.append(Finding(
|
|
286
|
+
kind="binding-deactivated",
|
|
287
|
+
path=str(prim.GetPath()),
|
|
288
|
+
detail="bound to %s, which is deactivated; ComputeBoundMaterial "
|
|
289
|
+
"still returns it" % target,
|
|
290
|
+
verdict=DEAD,
|
|
291
|
+
))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def empty_collections(stage: Usd.Stage, report: Report) -> None:
|
|
295
|
+
"""Collections that were authored with membership but now match nothing.
|
|
296
|
+
|
|
297
|
+
Katana's CEL failure mode, in USD: after a restructure the pattern matches
|
|
298
|
+
zero and nothing complains. Zero-match should be an error, not a no-op.
|
|
299
|
+
"""
|
|
300
|
+
for prim in stage.TraverseAll():
|
|
301
|
+
try:
|
|
302
|
+
collections = Usd.CollectionAPI.GetAllCollections(prim)
|
|
303
|
+
except Exception:
|
|
304
|
+
return # API not available in this USD build; skip the whole check
|
|
305
|
+
for coll in collections:
|
|
306
|
+
includes = coll.GetIncludesRel()
|
|
307
|
+
authored = bool(includes and includes.GetTargets())
|
|
308
|
+
expr_attr = coll.GetMembershipExpressionAttr()
|
|
309
|
+
if not authored and not (expr_attr and expr_attr.HasAuthoredValue()):
|
|
310
|
+
continue # nothing was ever asked for; not a finding
|
|
311
|
+
try:
|
|
312
|
+
query = coll.ComputeMembershipQuery()
|
|
313
|
+
included = Usd.CollectionAPI.ComputeIncludedPaths(query, stage)
|
|
314
|
+
except Exception:
|
|
315
|
+
continue
|
|
316
|
+
if not included:
|
|
317
|
+
report.findings.append(Finding(
|
|
318
|
+
kind="empty-collection",
|
|
319
|
+
path=str(coll.GetCollectionPath()),
|
|
320
|
+
detail="membership was authored but matches zero prims",
|
|
321
|
+
verdict=DEAD,
|
|
322
|
+
))
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
CHECKS = {
|
|
326
|
+
"stale-override": stale_overrides,
|
|
327
|
+
"override-in-instance": overrides_under_instances,
|
|
328
|
+
"dangling-target": dangling_targets,
|
|
329
|
+
"targets-stripped": stripped_targets,
|
|
330
|
+
"material-binding": material_bindings,
|
|
331
|
+
"empty-collection": empty_collections,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def scan(stage: Usd.Stage, anchor_key: str = ANCHOR_KEY) -> Report:
|
|
336
|
+
"""Run every check against a composed stage."""
|
|
337
|
+
report = Report()
|
|
338
|
+
anchors = anchor_index(stage, anchor_key)
|
|
339
|
+
report.anchors_found = len(anchors)
|
|
340
|
+
|
|
341
|
+
stale_overrides(stage, anchors, report)
|
|
342
|
+
overrides_under_instances(stage, report)
|
|
343
|
+
dangling_targets(stage, anchors, report)
|
|
344
|
+
stripped_targets(stage, report)
|
|
345
|
+
material_bindings(stage, report)
|
|
346
|
+
empty_collections(stage, report)
|
|
347
|
+
|
|
348
|
+
order = {DEAD: 0, MOVED: 1, UNKNOWN: 2}
|
|
349
|
+
report.findings.sort(key=lambda f: (order.get(f.verdict, 3), f.kind, f.path))
|
|
350
|
+
return report
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def scan_file(path: str, anchor_key: str = ANCHOR_KEY) -> Report:
|
|
354
|
+
stage = Usd.Stage.Open(path)
|
|
355
|
+
if not stage:
|
|
356
|
+
raise IOError("could not open stage: %s" % path)
|
|
357
|
+
return scan(stage, anchor_key)
|
deps/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""deps -- what depends on what, in USD, without opening a scene.
|
|
2
|
+
|
|
3
|
+
No stock USD API answers this. The composition-arc APIs report which *files* a
|
|
4
|
+
file needs and never see a material binding, a skel binding, an attribute
|
|
5
|
+
connection, or a class arc -- and none of them can tell you what points *at*
|
|
6
|
+
something, which is the question people actually ask before republishing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .graph import Cache, Graph, Unresolved, build, discover # noqa: F401
|
|
10
|
+
from .walk import ( # noqa: F401
|
|
11
|
+
ARC, ASSET_ATTR, COLLECTION, CONN, INHERIT, REL,
|
|
12
|
+
Edge, LayerRecord, walk_file, walk_layer,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Cache", "Graph", "Unresolved", "build", "discover",
|
|
17
|
+
"Edge", "LayerRecord", "walk_file", "walk_layer",
|
|
18
|
+
"ARC", "ASSET_ATTR", "COLLECTION", "CONN", "INHERIT", "REL",
|
|
19
|
+
]
|