From 932e690b655c1672102d73e52f2dba770a613643 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 16 Oct 2024 12:36:53 -0400 Subject: [PATCH] Implement slate_fill_mode for SlateHtmlProcessor. --- lablib/generators/__init__.py | 3 +- lablib/generators/slate_html.py | 100 +++++++++++------- .../slates/slate_generic/slate_generic.html | 6 +- tests/test_renderers_slate.py | 53 ++++++++-- 4 files changed, 111 insertions(+), 51 deletions(-) diff --git a/lablib/generators/__init__.py b/lablib/generators/__init__.py index bc62da4..89500fc 100644 --- a/lablib/generators/__init__.py +++ b/lablib/generators/__init__.py @@ -1,9 +1,10 @@ """Generator module to be used by Processors and Renderers.""" from .ocio_config import OCIOConfigFileGenerator -from .slate_html import SlateHtmlGenerator +from .slate_html import SlateHtmlGenerator, SlateFillMode __all__ = [ "OCIOConfigFileGenerator", "SlateHtmlGenerator", + "SlateFillMode" ] diff --git a/lablib/generators/slate_html.py b/lablib/generators/slate_html.py index 0c4bc3f..5816910 100644 --- a/lablib/generators/slate_html.py +++ b/lablib/generators/slate_html.py @@ -1,7 +1,8 @@ from __future__ import annotations +import collections import datetime -import os +from enum import Enum import shutil from typing import List, Dict from pathlib import Path @@ -13,21 +14,31 @@ from ..lib import utils, imageio +class SlateFillMode(Enum): + """Slate fill mode (fill up template from data). + """ + RAISE_WHEN_MISSING = 1 # missing data from template : raise + HIDE_FIELD_WHEN_MISSING = 2 # missing data from template : hide field + + class SlateHtmlGenerator: """Class to generate a slate from a template. Attributes: - slate_template_path: The path to the template. - data: A dictionary containing the data to be formatted in the template. - width: The width of the slate. - height: The height of the slate. - staging_dir: The directory where the slate will be staged. - source_files: A list of source files. - is_source_linear: A boolean to set whether the source files are linear. + data (dict): A dictionary containing the data to be formatted in the template. + slate_template_path (str): The path to the template. + width (int): The width of the slate. + height (int): The height of the slate. + staging_dir (str): The directory where the slate will be staged. + source_files (list): A list of source files. + is_source_linear (bool): A boolean to set whether the source files are linear. + slate_fill_mode (SlateFillMode): The template fill mode. Raises: ValueError: When the provided slate template path is invalid. """ + _MISSING_FIELD = "**MISSING**" + def __init__( self, data: Dict, @@ -36,7 +47,8 @@ def __init__( height: int = None, staging_dir: str = None, source_files: List = None, - is_source_linear: bool = None + is_source_linear: bool = None, + slate_fill_mode: SlateFillMode = None ): self.data = data self.width = width or 1920 @@ -44,6 +56,7 @@ def __init__( self._staging_dir = staging_dir or utils.get_staging_dir() self.source_files = source_files or [] self.is_source_linear = is_source_linear if is_source_linear is not None else True + self._slate_fill_mode = slate_fill_mode or SlateFillMode.RAISE_WHEN_MISSING try: slate_template_path = slate_template_path @@ -156,6 +169,35 @@ def _stage_slate(self) -> str: self._slate_staged_path = slate_staged_path.as_posix() return self._slate_staged_path + def __format_template(self, template_content: str, values: dict) -> str: + """ + Args: + template_content (str): The template content to format. + values (dict): The values to use for formatting. + + Returns: + str: The formatted string. + + Raises: + ValueError: When some key is missing or the mode is unknown. + """ + # Attempt to replace/format template with content. + if self._slate_fill_mode == SlateFillMode.RAISE_WHEN_MISSING: + try: + return template_content.format(**values) + except KeyError as error: + raise ValueError( + f"Key mismatch, cannot fill template: {error}" + ) from error + + elif self._slate_fill_mode == SlateFillMode.HIDE_FIELD_WHEN_MISSING: + default_values = collections.defaultdict(lambda: self._MISSING_FIELD) + default_values.update(values) + return template_content.format_map(default_values) + + else: + raise ValueError(f"Unknown slate fill mode {self._slate_fill_mode}.") + def _format_slate(self) -> None: """Format template with generator data values. @@ -179,13 +221,7 @@ def _format_slate(self) -> None: with open(self._slate_staged_path, "r+") as f: template_content = f.read() - # Attempt to replace/format template with content. - try: - content = template_content.format(**formatted_dict) - except KeyError as error: - raise ValueError( - f"Key mismatch, cannot fill template: {error}" - ) from error + content = self.__format_template(template_content, formatted_dict) # Override template file content with formatted data. with open(self._slate_staged_path, "w+") as f: @@ -195,27 +231,17 @@ def _format_slate(self) -> None: self._driver.get(self._slate_staged_path) - # TODO: Revisit this. - # Currently this function will fail with a KeyError - # if any data expected by the template is missing from - # the data dict. - # - # The code below turns this into a silent fails where - # missig fields are hidden from the resulting slate. -# elements = self._driver.find_elements( -# By.XPATH, -# "//*[contains(text(),'{}')]".format("**MISSING"), -# ) -# for el in elements: -# self._driver.execute_script( -# "var element = arguments[0];\n" "element.style.display = 'none';", el -# ) -# if self._remove_missing_parents: -# parent = el.find_element(By.XPATH, "..") -# self._driver.execute_script( -# "var element = arguments[0];\n" "element.style.display = 'none';", -# parent, -# ) + # HIDE_FIELD_WHEN_MISSING mode + # The code below hide detected missing fields from the resulting slate. + elements = self._driver.find_elements( + By.XPATH, + "//*[contains(text(),'{}')]".format(self._MISSING_FIELD), + ) + for el in elements: + self._driver.execute_script( + "var element = arguments[0];\n" "element.style.display = 'none';", el + ) + with open(self._slate_staged_path, "w") as f: f.write(self._driver.page_source) diff --git a/templates/slates/slate_generic/slate_generic.html b/templates/slates/slate_generic/slate_generic.html index c7d6cf0..5f85f0e 100644 --- a/templates/slates/slate_generic/slate_generic.html +++ b/templates/slates/slate_generic/slate_generic.html @@ -6,8 +6,8 @@
-
{project[name]}
-
{asset}_{task[short]}_v{@version}
+
{project_name}
+
{asset}_{task_short}_v{@version}
Date:
{dd} {mmm} {yyyy}
@@ -26,7 +26,7 @@
Intent:
-
{intent[value]}
+
{intent}
Notes:
diff --git a/tests/test_renderers_slate.py b/tests/test_renderers_slate.py index 7ede471..64aa71f 100644 --- a/tests/test_renderers_slate.py +++ b/tests/test_renderers_slate.py @@ -6,7 +6,7 @@ import pytest from lablib.lib import SequenceInfo -from lablib.generators import SlateHtmlGenerator +from lablib.generators import SlateHtmlGenerator, SlateFillMode from lablib.renderers import SlateRenderer @@ -56,6 +56,7 @@ def source_dir(): # Remove all temporary directory content. shutil.rmtree(temp_dir) + def test_Slaterenderer_missing_keys(): """ An Exception should raise if any key defined in the template but missing in the provided data. @@ -68,15 +69,47 @@ def test_Slaterenderer_missing_keys(): _run_slate_renderer(generator) +def test_Slaterenderer_unknown_mode(): + """ An Exception should raise if the provided slate mode is unknown. + """ + with pytest.raises(ValueError): + generator = SlateHtmlGenerator( + {"missing": "data"}, + SLATE_TEMPLATE_FILE, + slate_fill_mode="UNKNOWN MODE" + ) + _run_slate_renderer(generator) + + +def test_Slaterenderer_missing_keys_hide(source_dir): + """ Slate should go through even with missing data + when using HIDE_WHEN_MISSING mode. + """ + source_sequence = SequenceInfo.scan(source_dir)[0] + generator = SlateHtmlGenerator( + {"missing": "data"}, + SLATE_TEMPLATE_FILE, + slate_fill_mode=SlateFillMode.HIDE_FIELD_WHEN_MISSING + ) + _run_slate_renderer(generator, sequence=source_sequence) + + edited_sequence = SequenceInfo.scan(source_dir)[0] + slate_frame = edited_sequence.frames[0] + + assert len(edited_sequence.frames) == len(source_sequence.frames) + 1 + assert slate_frame.width == 1920 + assert slate_frame.height == 1080 + + def test_Slaterenderer_default(source_dir): """ Ensure a valid HD slate is generated from default. """ source_sequence = SequenceInfo.scan(source_dir)[0] generator = SlateHtmlGenerator( { - "project": {"name": "test_project"}, - "intent": {"value": "test_intent"}, - "task": {"short": "test_task"}, + "project_name": "test_project", + "intent": "test_intent", + "task_short": "test_task", "asset": "test_asset", "comment": "some random comment", "scope": "test_scope", @@ -100,9 +133,9 @@ def test_Slaterenderer_4K(source_dir): source_sequence = SequenceInfo.scan(source_dir)[0] generator = SlateHtmlGenerator( { - "project": {"name": "test_project"}, - "intent": {"value": "test_intent"}, - "task": {"short": "test_task"}, + "project_name": "test_project", + "intent": "test_intent", + "task_short": "test_task", "asset": "test_asset", "comment": "some random comment", "scope": "test_scope", @@ -128,9 +161,9 @@ def test_Slaterenderer_explicit_output(source_dir): expected_output = pathlib.Path(source_dir) / "output.exr" generator = SlateHtmlGenerator( { - "project": {"name": "test_project"}, - "intent": {"value": "test_intent"}, - "task": {"short": "test_task"}, + "project_name": "test_project", + "intent": "test_intent", + "task_short": "test_task", "asset": "test_asset", "comment": "some random comment", "scope": "test_scope",