feat: add ProjectResolution node
Implements ProjectResolution with TDD: fetches a [width, height] pair from a resolution series by loop index, clamping out-of-bounds indices to the last entry and returning (512, 512) defaults on error or missing key. Also registers the node in mappings and updates TestNodeMappings count to 4. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -293,15 +293,72 @@ class ProjectKey:
|
|||||||
return (str(val),)
|
return (str(val),)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectResolution:
|
||||||
|
"""Fetches a (width, height) pair from a resolution series by loop index."""
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"source_label": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"key_name": ("STRING", {"default": "resolutions", "multiline": False}),
|
||||||
|
"index": ("INT", {"default": 0, "min": 0, "max": 9999}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"manager_url": ("STRING", {"default": "http://localhost:8080", "multiline": False}),
|
||||||
|
"project_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"file_name": ("STRING", {"default": "", "multiline": False}),
|
||||||
|
"sequence_number": ("INT", {"default": 1, "min": 1, "max": 9999}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("INT", "INT")
|
||||||
|
RETURN_NAMES = ("width", "height")
|
||||||
|
FUNCTION = "fetch_resolution"
|
||||||
|
CATEGORY = "utils/json/project"
|
||||||
|
OUTPUT_NODE = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def IS_CHANGED(cls, **kwargs):
|
||||||
|
return float("nan")
|
||||||
|
|
||||||
|
def fetch_resolution(self, source_label, key_name, index,
|
||||||
|
manager_url="http://localhost:8080", project_name="",
|
||||||
|
file_name="", sequence_number=1):
|
||||||
|
sequence_number = int(sequence_number)
|
||||||
|
logger.info("ProjectResolution.fetch_resolution: source=%s key=%s url=%s project=%s file=%s seq=%s index=%s",
|
||||||
|
source_label, key_name, manager_url, project_name, file_name, sequence_number, index)
|
||||||
|
# source_label is used by JS to identify which ProjectSource to sync
|
||||||
|
# config from. The actual config arrives via the optional widgets below.
|
||||||
|
data = _fetch_data(manager_url, project_name, file_name, sequence_number)
|
||||||
|
if data.get("error") in ("http_error", "network_error", "parse_error"):
|
||||||
|
logger.warning("ProjectResolution.fetch_resolution failed: %s", data.get("message"))
|
||||||
|
return (512, 512)
|
||||||
|
|
||||||
|
series = data.get(key_name)
|
||||||
|
if not isinstance(series, list) or len(series) == 0:
|
||||||
|
logger.warning("ProjectResolution: key '%s' is not a resolution series", key_name)
|
||||||
|
return (512, 512)
|
||||||
|
|
||||||
|
clamped = max(0, min(index, len(series) - 1))
|
||||||
|
entry = series[clamped]
|
||||||
|
if not isinstance(entry, (list, tuple)) or len(entry) < 2:
|
||||||
|
logger.warning("ProjectResolution: entry at index %d is malformed: %r", clamped, entry)
|
||||||
|
return (512, 512)
|
||||||
|
|
||||||
|
return (to_int(entry[0]), to_int(entry[1]))
|
||||||
|
|
||||||
|
|
||||||
# --- Mappings ---
|
# --- Mappings ---
|
||||||
PROJECT_NODE_CLASS_MAPPINGS = {
|
PROJECT_NODE_CLASS_MAPPINGS = {
|
||||||
"ProjectLoaderDynamic": ProjectLoaderDynamic,
|
"ProjectLoaderDynamic": ProjectLoaderDynamic,
|
||||||
"ProjectSource": ProjectSource,
|
"ProjectSource": ProjectSource,
|
||||||
"ProjectKey": ProjectKey,
|
"ProjectKey": ProjectKey,
|
||||||
|
"ProjectResolution": ProjectResolution,
|
||||||
}
|
}
|
||||||
|
|
||||||
PROJECT_NODE_DISPLAY_NAME_MAPPINGS = {
|
PROJECT_NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"ProjectLoaderDynamic": "Project Loader (Dynamic)",
|
"ProjectLoaderDynamic": "Project Loader (Dynamic)",
|
||||||
"ProjectSource": "Project Source",
|
"ProjectSource": "Project Source",
|
||||||
"ProjectKey": "Project Key",
|
"ProjectKey": "Project Key",
|
||||||
|
"ProjectResolution": "Project Resolution",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,11 +344,102 @@ class TestProjectKey:
|
|||||||
assert ProjectKey.CATEGORY == "utils/json/project"
|
assert ProjectKey.CATEGORY == "utils/json/project"
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectResolution:
|
||||||
|
def test_input_types(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
inputs = ProjectResolution.INPUT_TYPES()
|
||||||
|
assert "source_label" in inputs["required"]
|
||||||
|
assert "key_name" in inputs["required"]
|
||||||
|
assert "index" in inputs["required"]
|
||||||
|
assert inputs["required"]["index"][0] == "INT"
|
||||||
|
|
||||||
|
def test_two_outputs(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
assert ProjectResolution.RETURN_TYPES == ("INT", "INT")
|
||||||
|
assert ProjectResolution.RETURN_NAMES == ("width", "height")
|
||||||
|
|
||||||
|
def test_fetch_resolution_basic(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
node = ProjectResolution()
|
||||||
|
data = {"resolutions": [[512, 512], [768, 1344], [1344, 768]]}
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_resolution(
|
||||||
|
source_label="src", key_name="resolutions", index=1,
|
||||||
|
manager_url="http://localhost:8080", project_name="p",
|
||||||
|
file_name="f", sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (768, 1344)
|
||||||
|
|
||||||
|
def test_fetch_resolution_index_zero(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
node = ProjectResolution()
|
||||||
|
data = {"resolutions": [[512, 512], [1024, 1024]]}
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_resolution(
|
||||||
|
source_label="src", key_name="resolutions", index=0,
|
||||||
|
manager_url="http://localhost:8080", project_name="p",
|
||||||
|
file_name="f", sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (512, 512)
|
||||||
|
|
||||||
|
def test_fetch_resolution_clamps_on_out_of_bounds(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
node = ProjectResolution()
|
||||||
|
data = {"resolutions": [[512, 512], [1024, 1024]]}
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_resolution(
|
||||||
|
source_label="src", key_name="resolutions", index=99,
|
||||||
|
manager_url="http://localhost:8080", project_name="p",
|
||||||
|
file_name="f", sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (1024, 1024) # last entry
|
||||||
|
|
||||||
|
def test_fetch_resolution_missing_key_returns_defaults(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
node = ProjectResolution()
|
||||||
|
with patch("project_loader._fetch_data", return_value={}):
|
||||||
|
result = node.fetch_resolution(
|
||||||
|
source_label="src", key_name="nonexistent", index=0,
|
||||||
|
manager_url="http://localhost:8080", project_name="p",
|
||||||
|
file_name="f", sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (512, 512)
|
||||||
|
|
||||||
|
def test_fetch_resolution_network_error_returns_defaults(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
node = ProjectResolution()
|
||||||
|
error_resp = {"error": "network_error", "message": "Connection refused"}
|
||||||
|
with patch("project_loader._fetch_data", return_value=error_resp):
|
||||||
|
result = node.fetch_resolution(
|
||||||
|
source_label="src", key_name="resolutions", index=0,
|
||||||
|
manager_url="http://localhost:8080", project_name="p",
|
||||||
|
file_name="f", sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (512, 512)
|
||||||
|
|
||||||
|
def test_fetch_resolution_malformed_entry_returns_defaults(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
node = ProjectResolution()
|
||||||
|
data = {"resolutions": [[512]]} # single-element, not a valid pair
|
||||||
|
with patch("project_loader._fetch_data", return_value=data):
|
||||||
|
result = node.fetch_resolution(
|
||||||
|
source_label="src", key_name="resolutions", index=0,
|
||||||
|
manager_url="http://localhost:8080", project_name="p",
|
||||||
|
file_name="f", sequence_number=1,
|
||||||
|
)
|
||||||
|
assert result == (512, 512)
|
||||||
|
|
||||||
|
def test_category(self):
|
||||||
|
from project_loader import ProjectResolution
|
||||||
|
assert ProjectResolution.CATEGORY == "utils/json/project"
|
||||||
|
|
||||||
|
|
||||||
class TestNodeMappings:
|
class TestNodeMappings:
|
||||||
def test_mappings_exist(self):
|
def test_mappings_exist(self):
|
||||||
from project_loader import PROJECT_NODE_CLASS_MAPPINGS, PROJECT_NODE_DISPLAY_NAME_MAPPINGS
|
from project_loader import PROJECT_NODE_CLASS_MAPPINGS, PROJECT_NODE_DISPLAY_NAME_MAPPINGS
|
||||||
assert "ProjectLoaderDynamic" in PROJECT_NODE_CLASS_MAPPINGS
|
assert "ProjectLoaderDynamic" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
assert "ProjectSource" in PROJECT_NODE_CLASS_MAPPINGS
|
assert "ProjectSource" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
assert "ProjectKey" in PROJECT_NODE_CLASS_MAPPINGS
|
assert "ProjectKey" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
assert len(PROJECT_NODE_CLASS_MAPPINGS) == 3
|
assert "ProjectResolution" in PROJECT_NODE_CLASS_MAPPINGS
|
||||||
assert len(PROJECT_NODE_DISPLAY_NAME_MAPPINGS) == 3
|
assert len(PROJECT_NODE_CLASS_MAPPINGS) == 4
|
||||||
|
assert len(PROJECT_NODE_DISPLAY_NAME_MAPPINGS) == 4
|
||||||
|
|||||||
Reference in New Issue
Block a user