Skip to content

Commit 3a5ff33

Browse files
committed
Added support for custom adapter hooks
This adds support for attributing custom hooks to adapters and executing them with `hook_function_argument_map` being passed along through the adapter IO functions. Signed-off-by: Tim Lehr <tim.lehr@disneyanimation.com>
1 parent 987d00a commit 3a5ff33

7 files changed

+126
-28
lines changed

src/py-opentimelineio/opentimelineio/adapters/adapter.py

+44-17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import inspect
1111
import collections
1212
import copy
13+
from typing import List
1314

1415
from .. import (
1516
core,
@@ -99,6 +100,21 @@ def read_from_file(
99100
media_linker_argument_map or {}
100101
)
101102

103+
hook_function_argument_map = copy.deepcopy(
104+
hook_function_argument_map or {}
105+
)
106+
hook_function_argument_map['adapter_arguments'] = copy.deepcopy(
107+
adapter_argument_map
108+
)
109+
hook_function_argument_map['media_linker_argument_map'] = (
110+
media_linker_argument_map
111+
)
112+
113+
if self.has_feature("hooks"):
114+
adapter_argument_map[
115+
"hook_function_argument_map"
116+
] = hook_function_argument_map
117+
102118
result = None
103119

104120
if (
@@ -119,15 +135,6 @@ def read_from_file(
119135
**adapter_argument_map
120136
)
121137

122-
hook_function_argument_map = copy.deepcopy(
123-
hook_function_argument_map or {}
124-
)
125-
hook_function_argument_map['adapter_arguments'] = copy.deepcopy(
126-
adapter_argument_map
127-
)
128-
hook_function_argument_map['media_linker_argument_map'] = (
129-
media_linker_argument_map
130-
)
131138
result = hooks.run(
132139
"post_adapter_read",
133140
result,
@@ -174,6 +181,11 @@ def write_to_file(
174181
# Store file path for use in hooks
175182
hook_function_argument_map['_filepath'] = filepath
176183

184+
if self.has_feature("hooks"):
185+
adapter_argument_map[
186+
"hook_function_argument_map"
187+
] = hook_function_argument_map
188+
177189
input_otio = hooks.run("pre_adapter_write", input_otio,
178190
extra_args=hook_function_argument_map)
179191
if (
@@ -210,13 +222,6 @@ def read_from_string(
210222
**adapter_argument_map
211223
):
212224
"""Call the read_from_string function on this adapter."""
213-
214-
result = self._execute_function(
215-
"read_from_string",
216-
input_str=input_str,
217-
**adapter_argument_map
218-
)
219-
220225
hook_function_argument_map = copy.deepcopy(
221226
hook_function_argument_map or {}
222227
)
@@ -227,6 +232,17 @@ def read_from_string(
227232
media_linker_argument_map
228233
)
229234

235+
if self.has_feature("hooks"):
236+
adapter_argument_map[
237+
"hook_function_argument_map"
238+
] = hook_function_argument_map
239+
240+
result = self._execute_function(
241+
"read_from_string",
242+
input_str=input_str,
243+
**adapter_argument_map
244+
)
245+
230246
result = hooks.run(
231247
"post_adapter_read",
232248
result,
@@ -277,6 +293,16 @@ def write_to_string(
277293
**adapter_argument_map
278294
)
279295

296+
def adapter_hook_names(self) -> List[str]:
297+
"""Returns a list of hooks claimed by the adapter.
298+
299+
In addition to the hook being declared in the manifest, it should also be
300+
returned here, so it can be attributed to the adapter.
301+
"""
302+
if not self.has_feature("hooks"):
303+
return []
304+
return self._execute_function("adapter_hook_names")
305+
280306
def __str__(self):
281307
return (
282308
"Adapter("
@@ -372,5 +398,6 @@ def _with_linked_media_references(
372398
'read': ['read_from_file', 'read_from_string'],
373399
'write_to_file': ['write_to_file'],
374400
'write_to_string': ['write_to_string'],
375-
'write': ['write_to_file', 'write_to_string']
401+
'write': ['write_to_file', 'write_to_string'],
402+
'hooks': ['adapter_hook_names']
376403
}

tests/baselines/adapter_plugin_manifest.plugin_manifest.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
},
1717
{
1818
"FROM_TEST_FILE" : "post_write_hookscript_example.json"
19+
},
20+
{
21+
"FROM_TEST_FILE" : "custom_adapter_hookscript_example.json"
1922
}
2023
],
2124
"hooks" : {
2225
"pre_adapter_write" : ["example hook", "example hook"],
2326
"post_adapter_read" : [],
2427
"post_adapter_write" : ["post write example hook"],
25-
"post_media_linker" : ["example hook"]
28+
"post_media_linker" : ["example hook"],
29+
"custom_adapter_hook": ["custom adapter hook"]
2630
},
2731
"version_manifests" : {
2832
"TEST_FAMILY_NAME": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"OTIO_SCHEMA" : "HookScript.1",
3+
"name" : "custom adapter hook",
4+
"filepath" : "custom_adapter_hookscript_example.py"
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright Contributors to the OpenTimelineIO project
3+
4+
"""This file is here to support the test_adapter_plugin unittest, specifically adapters
5+
that implement their own hooks.
6+
If you want to learn how to write your own adapter plugin, please read:
7+
https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html
8+
"""
9+
10+
11+
def hook_function(in_timeline, argument_map=None):
12+
in_timeline.metadata["custom_hook"] = dict(argument_map)
13+
return in_timeline

tests/baselines/example.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,41 @@
66
https://opentimelineio.readthedocs.io/en/latest/tutorials/write-an-adapter.html
77
"""
88

9-
import opentimelineio as otio
109

10+
# `hook_function_argument_map` is only a required argument for adapters that implement
11+
# custom hooks.
12+
def read_from_file(filepath, suffix="", hook_function_argument_map=None):
13+
import opentimelineio as otio
1114

12-
def read_from_file(filepath, suffix=""):
1315
fake_tl = otio.schema.Timeline(name=filepath + str(suffix))
1416
fake_tl.tracks.append(otio.schema.Track())
1517
fake_tl.tracks[0].append(otio.schema.Clip(name=filepath + "_clip"))
18+
19+
if (hook_function_argument_map and
20+
hook_function_argument_map.get("run_custom_hook", False)):
21+
return otio.hooks.run(hook="custom_adapter_hook", tl=fake_tl,
22+
extra_args=hook_function_argument_map)
23+
1624
return fake_tl
1725

1826

19-
def read_from_string(input_str, suffix=""):
20-
return read_from_file(input_str, suffix)
27+
# `hook_function_argument_map` is only a required argument for adapters that implement
28+
# custom hooks.
29+
def read_from_string(input_str, suffix="", hook_function_argument_map=None):
30+
tl = read_from_file(input_str, suffix, hook_function_argument_map)
31+
return tl
32+
33+
34+
# this is only required for adapters that implement custom hooks
35+
def adapter_hook_names():
36+
return ["custom_adapter_hook"]
2137

2238

2339
# in practice, these will be in separate plugins, but for simplicity in the
2440
# unit tests, we put this in the same file as the example adapter.
2541
def link_media_reference(in_clip, media_linker_argument_map):
42+
import opentimelineio as otio
43+
2644
d = {'from_test_linker': True}
2745
d.update(media_linker_argument_map)
2846
return otio.schema.MissingReference(

tests/test_adapter_plugin.py

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def test_has_feature(self):
8989
self.assertTrue(self.adp.has_feature("read"))
9090
self.assertTrue(self.adp.has_feature("read_from_file"))
9191
self.assertFalse(self.adp.has_feature("write"))
92+
self.assertTrue(self.adp.has_feature("hooks"))
9293

9394
def test_pass_arguments_to_adapter(self):
9495
self.assertEqual(self.adp.read_from_file("foo", suffix=3).name, "foo3")

tests/test_hooks_plugins.py

+36-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
HOOKSCRIPT_PATH = "hookscript_example"
2121
POST_WRITE_HOOKSCRIPT_PATH = "post_write_hookscript_example"
22+
CUSTOM_ADAPTER_HOOKSCRIPT_PATH = "custom_adapter_hookscript_example"
2223

2324
POST_RUN_NAME = "hook ran and did stuff"
2425
TEST_METADATA = {'extra_data': True}
@@ -71,8 +72,17 @@ def setUp(self):
7172
"baselines",
7273
POST_WRITE_HOOKSCRIPT_PATH
7374
)
74-
self.man.hook_scripts = [self.hsf, self.post_hsf]
75-
75+
self.adapter_hook_jsn = baseline_reader.json_baseline_as_string(
76+
CUSTOM_ADAPTER_HOOKSCRIPT_PATH
77+
)
78+
self.adapter_hookscript = otio.adapters.otio_json.read_from_string(
79+
self.adapter_hook_jsn)
80+
self.adapter_hookscript._json_path = os.path.join(
81+
baseline_reader.MODPATH,
82+
"baselines",
83+
HOOKSCRIPT_PATH
84+
)
85+
self.man.hook_scripts = [self.hsf, self.post_hsf, self.adapter_hookscript]
7686
self.orig_manifest = otio.plugins.manifest._MANIFEST
7787
otio.plugins.manifest._MANIFEST = self.man
7888

@@ -83,6 +93,8 @@ def tearDown(self):
8393
def test_plugin_adapter(self):
8494
self.assertEqual(self.hsf.name, "example hook")
8595
self.assertEqual(self.hsf.filepath, "example.py")
96+
self.assertEqual(otio.adapters.from_name("example").adapter_hook_names(),
97+
["custom_adapter_hook"])
8698

8799
def test_load_adapter_module(self):
88100
target = os.path.join(
@@ -101,15 +113,25 @@ def test_run_hook_function(self):
101113
self.assertEqual(result.name, POST_RUN_NAME)
102114
self.assertEqual(result.metadata.get("extra_data"), True)
103115

116+
def test_run_custom_hook_function(self):
117+
tl = otio.schema.Timeline()
118+
result = otio.hooks.run(hook="custom_adapter_hook", tl=tl,
119+
extra_args=TEST_METADATA)
120+
self.assertEqual(result.metadata["custom_hook"], TEST_METADATA)
121+
104122
def test_run_hook_through_adapters(self):
123+
hook_map = dict(TEST_METADATA)
124+
hook_map["run_custom_hook"] = True
125+
105126
result = otio.adapters.read_from_string(
106127
'foo', adapter_name='example',
107128
media_linker_name='example',
108-
hook_function_argument_map=TEST_METADATA
129+
hook_function_argument_map=hook_map
109130
)
110131

111132
self.assertEqual(result.name, POST_RUN_NAME)
112133
self.assertEqual(result.metadata.get("extra_data"), True)
134+
self.assertEqual(result.metadata["custom_hook"]["extra_data"], True)
113135

114136
def test_post_write_hook(self):
115137
self.man.adapters.extend(self.orig_manifest.adapters)
@@ -161,19 +183,20 @@ def test_available_hookscript_names(self):
161183
# for not just assert that it returns a non-empty list
162184
self.assertEqual(
163185
list(otio.hooks.available_hookscripts()),
164-
[self.hsf, self.post_hsf]
186+
[self.hsf, self.post_hsf, self.adapter_hookscript]
165187
)
166188
self.assertEqual(
167189
otio.hooks.available_hookscript_names(),
168-
[self.hsf.name, self.post_hsf.name]
190+
[self.hsf.name, self.post_hsf.name, self.adapter_hookscript.name]
169191
)
170192

171193
def test_manifest_hooks(self):
172194
self.assertEqual(
173195
sorted(list(otio.hooks.names())),
174196
sorted(
175197
["post_adapter_read", "post_media_linker",
176-
"pre_adapter_write", "post_adapter_write"]
198+
"pre_adapter_write", "post_adapter_write",
199+
"custom_adapter_hook"]
177200
)
178201
)
179202

@@ -204,6 +227,13 @@ def test_manifest_hooks(self):
204227
]
205228
)
206229

230+
self.assertEqual(
231+
list(otio.hooks.scripts_attached_to("custom_adapter_hook")),
232+
[
233+
self.adapter_hookscript.name
234+
]
235+
)
236+
207237
tl = otio.schema.Timeline()
208238
result = otio.hooks.run("pre_adapter_write", tl, TEST_METADATA)
209239
self.assertEqual(result.name, POST_RUN_NAME)

0 commit comments

Comments
 (0)