Skip to content

Commit 2fe1329

Browse files
feat: Run pre-GUI hooks in the Cinema 4D render submitter
Wire deadline-cloud's pre-GUI submission hooks into the Cinema 4D submitter so studios can pre-populate the submit dialog (job name/description, queue parameters, deadline: job properties) before it opens, matching the equivalent Maya and Nuke integrations. Submitter: - _show_submitter calls run_pre_gui_hooks(PreGuiHookContext( bundle_dir=None, submitter_name="cinema4d", ...)) before building SubmitJobToDeadlineDialog. C4D has no on-disk bundle at pre-GUI time, so hooks come from DEADLINE_HOOKS_DIR only, gated by settings.allow_environment_hooks. - _pre_gui_hook_confirm_callback selects the confirmation callback from settings.auto_accept (None to skip the prompt, else qt_hook_confirmation) so the branch is unit-testable headless. - Merged output applied via apply_pre_gui_output; since RenderSubmitterUISettings has no .parameters list, name/description land on the settings object and every hook parameter flows into the dialog's shared parameter values. - Declined confirmation (DeadlineOperationCanceled) aborts cleanly, and falsy hook output is a guarded no-op. - Bumps the deadline floor to >= 0.60.1 (first release shipping deadline.client.ui.pre_gui_hooks). Tests: - Unit (test_pre_gui_hooks.py): headless coverage of the DCC-owned mapping contract (name/description -> settings, params -> shared values, empty/falsy/partial output) and the auto_accept confirm callback. - Integration (test/integ/test_cinema4d.py::test_pre_gui_hook): drives the real submitter via xa11y with DEADLINE_HOOKS_DIR + a fixture hook and asserts the exported bundle carries the hook output (name="PREGUI RAN", description, deadline:priority=88). Adds the fixtures/pregui_hooks/ hook and test_cases/pregui_hook/ scene; extra_env plumbing + _enable_environment_hooks() enable the env-hook path against the mock backend. Validated on a Windows + Cinema 4D 2026 workstation (1 passed). Signed-off-by: Leon Li <2182521+leon-li-inspire@users.noreply.github.com>
1 parent aa148a6 commit 2fe1329

8 files changed

Lines changed: 439 additions & 8 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ classifiers = [
3030
]
3131

3232
dependencies = [
33-
"deadline[gui] >= 0.59, < 0.61",
33+
"deadline[gui] >= 0.60.1, < 0.61",
3434
"openjd-adaptor-runtime >= 0.7,< 0.10",
3535
# fonttools is Windows-only due to Cinema 4D technical limitations
3636
"fonttools >=4.59.2, <4.64; sys_platform == 'win32'",
3737
]
3838

3939
[project.optional-dependencies]
4040
gui = [
41-
"deadline[gui] >= 0.59, < 0.61",
41+
"deadline[gui] >= 0.60.1, < 0.61",
4242
"PySide6-Essentials == 6.8.3", # Use 6.8 to align with VFXP 2026: https://vfxplatform.com/#reference-platform
4343
]
4444

src/deadline/cinema4d_submitter/cinema4d_render_submitter.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,22 @@
1515
from qtpy import QtWidgets
1616
from qtpy.QtCore import Qt # type: ignore[attr-defined]
1717

18+
from deadline.client.config import get_setting, str2bool
1819
from deadline.client.dataclasses import SubmitterInfo
19-
from deadline.client.exceptions import DeadlineOperationError
20+
from deadline.client.exceptions import DeadlineOperationCanceled, DeadlineOperationError
2021
from deadline.client.job_bundle._yaml import deadline_yaml_dump
2122
from deadline.client.job_bundle.parameters import JobParameter
2223
from deadline.client.job_bundle.submission import AssetReferences
2324
from deadline.client.ui.dialogs.submit_job_to_deadline_dialog import ( # pylint: disable=import-error
2425
JobBundlePurpose,
2526
SubmitJobToDeadlineDialog,
2627
)
28+
from deadline.client.ui.pre_gui_hooks import (
29+
PreGuiHookContext,
30+
apply_pre_gui_output,
31+
qt_hook_confirmation,
32+
run_pre_gui_hooks,
33+
)
2734

2835
from ._version import version_tuple as adaptor_version_tuple
2936
from .assets import AssetIntrospector
@@ -111,6 +118,10 @@ def show_submitter():
111118
)
112119
else:
113120
w = _show_submitter(temp_dir, None)
121+
# _show_submitter returns None when the user declines the pre-GUI hook confirmation
122+
# prompt; treat that as a normal cancellation and skip showing the dialog.
123+
if w is None:
124+
return
114125
w.setStyleSheet(C4D_STYLE)
115126
w.exec_()
116127
except Exception:
@@ -933,6 +944,19 @@ def export_to_temp_folder(temp_dir: str, asset_references: AssetReferences) -> N
933944
asset_references.input_filenames = temp_assets
934945

935946

947+
def _pre_gui_hook_confirm_callback(parent):
948+
"""Choose the confirmation callback for pre-GUI hooks based on the auto_accept setting.
949+
950+
Returns ``None`` (run hooks without prompting) when ``settings.auto_accept`` is enabled,
951+
otherwise the standard Qt confirmation dialog from ``qt_hook_confirmation``. Kept as a small
952+
helper so the auto_accept branch can be unit-tested headlessly.
953+
"""
954+
if str2bool(get_setting("settings.auto_accept")):
955+
return None
956+
957+
return qt_hook_confirmation(parent)
958+
959+
936960
def _show_submitter(temp_dir: str, parent=None, f=Qt.WindowType.Tool): # type: ignore[call-overload]
937961
"""
938962
Creates and returns a submission dialog for rendering jobs.
@@ -1018,12 +1042,41 @@ def on_create_job_bundle_callback(
10181042
host_requirements,
10191043
)
10201044

1045+
shared_parameter_values = {
1046+
"CondaPackages": conda_packages,
1047+
}
1048+
1049+
# Run pre-GUI hooks so studios can pre-populate dialog fields before it opens. Cinema 4D has
1050+
# no on-disk job bundle at this point, so hooks are sourced from DEADLINE_HOOKS_DIR only
1051+
# (bundle_dir=None), gated by settings.allow_environment_hooks. The confirmation prompt is
1052+
# skipped when auto_accept is set; otherwise the standard dialog is shown.
1053+
try:
1054+
pre_gui_output = run_pre_gui_hooks(
1055+
PreGuiHookContext(
1056+
bundle_dir=None,
1057+
job_name=render_settings.name,
1058+
submitter_name="cinema4d",
1059+
parameters=dict(shared_parameter_values),
1060+
),
1061+
confirm_callback=_pre_gui_hook_confirm_callback(parent),
1062+
)
1063+
except DeadlineOperationCanceled:
1064+
# The user declined the hook confirmation prompt. This is a normal cancellation, not an
1065+
# error, so abort opening the submitter silently by returning None; show_submitter skips
1066+
# the dialog. Without this, the exception would surface as a spurious "Deadline UI launch
1067+
# failed" error for what is a deliberate "No" click.
1068+
return None
1069+
# RenderSubmitterUISettings has no `.parameters` list, so apply_pre_gui_output routes
1070+
# name/description onto it and every hook parameter into shared_parameter_values.
1071+
# run_pre_gui_hooks returns {} when no hooks run (and raises DeadlineOperationCanceled, handled
1072+
# above, if the user declines); `or {}` is defensive against any future contract change so the
1073+
# common no-hooks path can never pass a falsy value into apply_pre_gui_output.
1074+
apply_pre_gui_output(pre_gui_output or {}, render_settings, shared_parameter_values)
1075+
10211076
submitter_dialog = SubmitJobToDeadlineDialog(
10221077
job_setup_widget_type=SceneSettingsWidget,
10231078
initial_job_settings=render_settings,
1024-
initial_shared_parameter_values={
1025-
"CondaPackages": conda_packages,
1026-
},
1079+
initial_shared_parameter_values=shared_parameter_values,
10271080
auto_detected_attachments=auto_detected_attachments,
10281081
attachments=attachments,
10291082
on_create_job_bundle_callback=on_create_job_bundle_callback,

test/AGENTS.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,21 @@ the mock logs `404 NO ROUTE`). Add a `@route`-decorated handler in
344344
resource data, extend `MockDeadlineScenario` there. Keep DCC-specific launch
345345
behavior in this repository.
346346

347+
**Pre-GUI hook case (`test_pre_gui_hook`):** this one does *not* use the `_CASES`
348+
/ golden-bundle model — it's a standalone test asserting only the fields a
349+
pre-GUI hook owns. A hook fixture lives in `fixtures/pregui_hooks/` (a
350+
`hooks.yaml`, version `"1.0"`, plus `pregui_hook.py`, which reads the job
351+
metadata on stdin and emits `name` / `description` / `parameters` as JSON on
352+
stdout). The test enables `settings.allow_environment_hooks` (so the submitter
353+
sources `DEADLINE_HOOKS_DIR`) and `settings.auto_accept` (so hooks run without
354+
the Qt confirmation prompt) in the config the `deadline_farm` fixture wrote,
355+
points `DEADLINE_HOOKS_DIR` at the fixture via the launch env, Exports, and
356+
asserts the emitted `name`/`description` reached `template.yaml` and
357+
`deadline:priority` reached `parameter_values.yaml`. To change what the hook
358+
injects, edit `pregui_hook.py` and the `_HOOK_*` constants in `test_cinema4d.py`
359+
together (they're the paired source of truth). It has no `expected/job_bundle/`
360+
and never renders, so it needs no golden capture and runs on macOS too.
361+
347362
## Installer Tests
348363

349364
Test the built installer. Requires having run `hatch run installer:build-installer` first.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# Pre-GUI hook manifest for the Cinema 4D xa11y integ test. Discovered via DEADLINE_HOOKS_DIR
3+
# (gated by settings.allow_environment_hooks); version "1.0" is the schema the installed
4+
# deadline-cloud validator accepts. The submitter runs the preGUI hook before the dialog opens.
5+
version: "1.0"
6+
preGUI:
7+
- command: python
8+
args:
9+
- pregui_hook.py
10+
timeout: 60
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
"""Pre-GUI hook fixture for the Cinema 4D xa11y integ test.
3+
4+
deadline-cloud runs this as a subprocess before the submitter dialog is built (see
5+
``deadline.client.ui.pre_gui_hooks.run_pre_gui_hooks``). It receives the job metadata as JSON on
6+
stdin and returns the merged pre-GUI output as JSON on stdout: ``name`` / ``description`` land on
7+
the submitter's settings object, and every key under ``parameters`` flows into the dialog's shared
8+
parameter values (``deadline:priority`` sets the Priority field).
9+
10+
The test asserts the exported job bundle reflects exactly these values, proving the C4D submitter
11+
wires ``run_pre_gui_hooks`` + ``apply_pre_gui_output`` correctly (PR #480). Keep the emitted values
12+
in sync with ``expected/job_bundle/`` for the ``pregui_hook`` case.
13+
"""
14+
15+
import json
16+
import sys
17+
18+
# Consume the metadata C4D passes on stdin (jobName, parameters, submitterName, ...). We do not
19+
# branch on it here — the point of the fixture is a deterministic, asserted output — but reading it
20+
# keeps the subprocess contract honest (a real hook would use it). A malformed/empty stdin is not a
21+
# failure for this fixture: it still emits the fixed output below, so we deliberately ignore a
22+
# decode error rather than abort.
23+
try:
24+
json.load(sys.stdin)
25+
except (json.JSONDecodeError, ValueError):
26+
# No usable metadata on stdin; the fixture's output does not depend on it, so continue.
27+
pass
28+
29+
output = {
30+
"name": "PREGUI RAN",
31+
"description": "populated by pre-GUI hook",
32+
"parameters": {
33+
"deadline:priority": 88,
34+
},
35+
}
36+
37+
json.dump(output, sys.stdout)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
"""Build the scene for the pre-GUI hook integ case and save it.
3+
4+
Usage: c4dpy scene.py <scene_dir>
5+
6+
Mirrors the ``cube`` case's scene, but this case never renders (the pre-GUI hook
7+
test only exports a job bundle and asserts its metadata), so it omits the
8+
render-data setup. The submitter still needs a saved document with a path, which
9+
the test's sidecar plugin loads before opening the dialog.
10+
"""
11+
12+
import os
13+
import sys
14+
15+
import c4d
16+
17+
18+
def main() -> int:
19+
if len(sys.argv) < 2:
20+
print("usage: c4dpy scene.py <scene_dir>", file=sys.stderr)
21+
return 2
22+
23+
scene_dir = sys.argv[1]
24+
os.makedirs(scene_dir, exist_ok=True)
25+
26+
doc = c4d.documents.GetActiveDocument()
27+
doc.Flush()
28+
29+
cube = c4d.BaseObject(c4d.Ocube)
30+
cube[c4d.PRIM_CUBE_LEN] = c4d.Vector(200, 200, 200)
31+
cube.SetAbsPos(c4d.Vector(0, 100, 0))
32+
doc.InsertObject(cube)
33+
34+
scene_name = "pregui_hook.c4d"
35+
scene_path = os.path.join(scene_dir, scene_name)
36+
doc.SetDocumentPath(scene_dir)
37+
doc.SetDocumentName(scene_name)
38+
c4d.documents.SaveDocument(doc, scene_path, c4d.SAVEDOCUMENTFLAGS_0, c4d.FORMAT_C4DEXPORT)
39+
c4d.EventAdd()
40+
41+
print(f"saved: {scene_path}", flush=True)
42+
return 0
43+
44+
45+
if __name__ == "__main__":
46+
sys.exit(main())

0 commit comments

Comments
 (0)