mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-09 07:20:15 -03:00
fix(types): resolve pre-existing basedpyright errors in tests
Fix ~790 basedpyright errors across the test suite: - Type stub subclasses of real production classes with super().__init__() - Add missing generic type arguments and Dict[str, Any] annotations - Add None guards before subscript/member access - Adapt tests to production API changes (removed dead handlers, PersistentModelCache.get_default, _i18n_filter_added location)
This commit is contained in:
@@ -76,6 +76,7 @@ class TestRewritePreviewUrl:
|
||||
for url in test_cases:
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True
|
||||
assert result is not None
|
||||
assert "width=450,optimized=true" in result
|
||||
|
||||
def test_handles_urls_with_explicit_port(self):
|
||||
@@ -83,6 +84,7 @@ class TestRewritePreviewUrl:
|
||||
url = "https://image.civitai.com:443/checkpoints/original=true"
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True
|
||||
assert result is not None
|
||||
assert "width=450,optimized=true" in result
|
||||
# Port is preserved in the URL (this is acceptable behavior)
|
||||
assert ":443" in result
|
||||
@@ -104,6 +106,8 @@ class TestRewritePreviewUrl:
|
||||
result2, was2 = rewrite_preview_url(url, "Video")
|
||||
assert was1 is True
|
||||
assert was2 is True
|
||||
assert result1 is not None
|
||||
assert result2 is not None
|
||||
assert "transcode=true" in result1
|
||||
assert "transcode=true" in result2
|
||||
|
||||
@@ -119,6 +123,7 @@ class TestRewritePreviewUrl:
|
||||
url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.png"
|
||||
result, was_rewritten = rewrite_preview_url(url, "image")
|
||||
assert was_rewritten is True
|
||||
assert result is not None
|
||||
assert result.startswith(
|
||||
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/"
|
||||
)
|
||||
@@ -129,6 +134,7 @@ class TestRewritePreviewUrl:
|
||||
url = "https://image.civitai.com/original=true/test.png"
|
||||
result, was_rewritten = rewrite_preview_url(url, None)
|
||||
assert was_rewritten is True
|
||||
assert result is not None
|
||||
assert "transcode=true" not in result
|
||||
assert "width=450,optimized=true" in result
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Generator
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -19,7 +19,7 @@ class RecordingWebSocketManager:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_settings() -> None:
|
||||
def restore_settings() -> Generator[None, None, None]:
|
||||
manager = get_settings_manager()
|
||||
original = manager.settings.copy()
|
||||
try:
|
||||
@@ -45,7 +45,9 @@ async def test_start_download_requires_configured_path(
|
||||
|
||||
result = await manager.start_download({"auto_mode": True})
|
||||
assert result["success"] is True
|
||||
assert "skipping auto download" in result["message"]
|
||||
message = result["message"]
|
||||
assert isinstance(message, str)
|
||||
assert "skipping auto download" in message
|
||||
|
||||
|
||||
async def test_start_download_bootstraps_progress_and_task(
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Generator
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -20,8 +20,13 @@ class JsonRequest:
|
||||
return self._payload
|
||||
|
||||
|
||||
def _parse_json(response) -> Dict[str, Any]:
|
||||
assert response.text is not None
|
||||
return json.loads(response.text)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_settings() -> None:
|
||||
def restore_settings() -> Generator[None, None, None]:
|
||||
manager = get_settings_manager()
|
||||
original = manager.settings.copy()
|
||||
try:
|
||||
@@ -54,7 +59,7 @@ async def test_open_folder_requires_existing_model_directory(monkeypatch: pytest
|
||||
|
||||
request = JsonRequest({"model_hash": model_hash})
|
||||
response = await ExampleImagesFileManager.open_folder(request)
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert body["success"] is True
|
||||
# On Windows, os.startfile is used; on other platforms, subprocess.Popen
|
||||
@@ -89,7 +94,7 @@ async def test_open_folder_returns_clipboard_mode_with_mapped_local_path(
|
||||
|
||||
request = JsonRequest({"model_hash": model_hash})
|
||||
response = await ExampleImagesFileManager.open_folder(request)
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert body == {
|
||||
@@ -126,7 +131,7 @@ async def test_open_folder_returns_uri_mode_with_rendered_template(
|
||||
|
||||
request = JsonRequest({"model_hash": model_hash})
|
||||
response = await ExampleImagesFileManager.open_folder(request)
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert response.status == 200
|
||||
assert body["success"] is True
|
||||
@@ -150,7 +155,7 @@ async def test_open_folder_rejects_missing_uri_template(monkeypatch: pytest.Monk
|
||||
(model_folder / "image.png").write_text("data", encoding="utf-8")
|
||||
|
||||
response = await ExampleImagesFileManager.open_folder(JsonRequest({"model_hash": model_hash}))
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert response.status == 400
|
||||
assert body["success"] is False
|
||||
@@ -168,7 +173,7 @@ async def test_open_folder_rejects_invalid_paths(monkeypatch: pytest.MonkeyPatch
|
||||
|
||||
request = JsonRequest({"model_hash": "a" * 64})
|
||||
response = await ExampleImagesFileManager.open_folder(request)
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert response.status == 400
|
||||
assert body["success"] is False
|
||||
@@ -186,7 +191,7 @@ async def test_get_files_lists_supported_media(tmp_path) -> None:
|
||||
|
||||
request = JsonRequest({}, {"model_hash": model_hash})
|
||||
response = await ExampleImagesFileManager.get_files(request)
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert response.status == 200
|
||||
names = {entry["name"] for entry in body["files"]}
|
||||
@@ -203,18 +208,18 @@ async def test_has_images_reports_presence(tmp_path) -> None:
|
||||
|
||||
request = JsonRequest({}, {"model_hash": model_hash})
|
||||
response = await ExampleImagesFileManager.has_images(request)
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
|
||||
assert body["has_images"] is True
|
||||
|
||||
empty_request = JsonRequest({}, {"model_hash": "missing"})
|
||||
empty_response = await ExampleImagesFileManager.has_images(empty_request)
|
||||
empty_body = json.loads(empty_response.text)
|
||||
empty_body = _parse_json(empty_response)
|
||||
assert empty_body["has_images"] is False
|
||||
|
||||
|
||||
async def test_has_images_requires_model_hash() -> None:
|
||||
response = await ExampleImagesFileManager.has_images(JsonRequest({}, {}))
|
||||
body = json.loads(response.text)
|
||||
body = _parse_json(response)
|
||||
assert response.status == 400
|
||||
assert body["success"] is False
|
||||
|
||||
@@ -102,7 +102,7 @@ async def test_update_metadata_after_import_preserves_existing_metadata(
|
||||
model_file.write_text("content", encoding="utf-8")
|
||||
metadata_path = tmp_path / "preserve.metadata.json"
|
||||
|
||||
existing_payload = {
|
||||
existing_payload: Dict[str, Any] = {
|
||||
"model_name": "Example",
|
||||
"file_path": str(model_file),
|
||||
"civitai": {
|
||||
@@ -200,7 +200,7 @@ async def test_update_metadata_from_local_examples_generates_entries(monkeypatch
|
||||
model_dir = tmp_path / model_hash
|
||||
model_dir.mkdir()
|
||||
(model_dir / "image.png").write_text("data", encoding="utf-8")
|
||||
model_data = {"model_name": "Local", "civitai": {}, "file_path": str(tmp_path / "model.safetensors")}
|
||||
model_data: Dict[str, Any] = {"model_name": "Local", "civitai": {}, "file_path": str(tmp_path / "model.safetensors")}
|
||||
|
||||
async def fake_save(path, metadata):
|
||||
return True
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, Dict, Generator, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -15,7 +15,7 @@ from py.utils.example_images_paths import get_model_folder
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def restore_settings() -> None:
|
||||
def restore_settings() -> Generator[None, None, None]:
|
||||
manager = get_settings_manager()
|
||||
original = manager.settings.copy()
|
||||
try:
|
||||
@@ -206,7 +206,7 @@ async def test_import_images_creates_hash_directory(monkeypatch: pytest.MonkeyPa
|
||||
|
||||
monkeypatch.setattr(processor_module.MetadataUpdater, "update_metadata_after_import", staticmethod(fake_update_metadata))
|
||||
|
||||
result = await processor_module.ExampleImagesProcessor.import_images("a" * 64, [str(source_file)])
|
||||
result: Dict[str, Any] = await processor_module.ExampleImagesProcessor.import_images("a" * 64, [str(source_file)])
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["files"][0]["name"].startswith("custom_short")
|
||||
@@ -255,7 +255,7 @@ async def test_delete_custom_image_preserves_existing_metadata(monkeypatch: pyte
|
||||
model_file.write_text("content", encoding="utf-8")
|
||||
metadata_path = tmp_path / "keep.metadata.json"
|
||||
|
||||
existing_metadata = {
|
||||
existing_metadata: Dict[str, Any] = {
|
||||
"model_name": "Keep",
|
||||
"file_path": str(model_file),
|
||||
"civitai": {
|
||||
@@ -313,6 +313,7 @@ async def test_delete_custom_image_preserves_existing_metadata(monkeypatch: pyte
|
||||
)
|
||||
|
||||
assert response.status == 200
|
||||
assert response.text is not None
|
||||
body = json.loads(response.text)
|
||||
assert body["success"] is True
|
||||
assert body["custom_images"] == []
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
import piexif
|
||||
import piexif # pyright: ignore[reportMissingTypeStubs]
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
from py.utils.exif_utils import ExifUtils
|
||||
@@ -84,10 +85,12 @@ def test_optimize_image_preserves_workflow_when_converting_png_to_webp(tmp_path)
|
||||
optimized_path.write_bytes(optimized_data)
|
||||
|
||||
exif_dict = piexif.load(str(optimized_path))
|
||||
assert exif_dict["0th"] is not None
|
||||
assert (
|
||||
exif_dict["0th"][piexif.ImageIFD.ImageDescription].decode("utf-8")
|
||||
== 'Workflow:{"nodes": [{"id": 1}]}'
|
||||
)
|
||||
assert exif_dict["Exif"] is not None
|
||||
user_comment = exif_dict["Exif"][piexif.ExifIFD.UserComment]
|
||||
assert user_comment.startswith(b"UNICODE\0")
|
||||
assert user_comment[8:].decode("utf-16be") == "prompt text\nSteps: 20"
|
||||
@@ -113,10 +116,12 @@ def test_update_image_metadata_preserves_webp_workflow(tmp_path):
|
||||
)
|
||||
|
||||
updated_exif = piexif.load(str(image_path))
|
||||
assert updated_exif["0th"] is not None
|
||||
assert (
|
||||
updated_exif["0th"][piexif.ImageIFD.ImageDescription].decode("utf-8")
|
||||
== 'Workflow:{"nodes":[{"id":1}]}'
|
||||
)
|
||||
assert updated_exif["Exif"] is not None
|
||||
updated_comment = updated_exif["Exif"][piexif.ExifIFD.UserComment]
|
||||
assert (
|
||||
updated_comment[8:].decode("utf-16be")
|
||||
@@ -147,10 +152,10 @@ def test_update_image_metadata_preserves_png_workflow(tmp_path):
|
||||
|
||||
import struct
|
||||
|
||||
import brotli
|
||||
import brotli # pyright: ignore[reportMissingTypeStubs]
|
||||
|
||||
|
||||
def _build_jxl_with_brob(payload_json: dict) -> bytes:
|
||||
def _build_jxl_with_brob(payload_json: Dict[str, Any]) -> bytes:
|
||||
"""Build a minimal JXL container with a brob box containing brotli-compressed JSON."""
|
||||
# ISOBMFF box 1: JXL signature box (size=12, type='JXL ', signature)
|
||||
box1 = struct.pack(">I", 12) + b"JXL " + bytes([0x0d, 0x0a, 0x87, 0x0a])
|
||||
@@ -163,7 +168,7 @@ def _build_jxl_with_brob(payload_json: dict) -> bytes:
|
||||
return box1 + box2 + box3
|
||||
|
||||
|
||||
def _build_avif_with_brob(payload_json: dict) -> bytes:
|
||||
def _build_avif_with_brob(payload_json: Dict[str, Any]) -> bytes:
|
||||
"""Build a minimal AVIF container with a brob box containing brotli-compressed JSON."""
|
||||
compressed = brotli.compress(json.dumps(payload_json).encode("utf-8"))
|
||||
brob_payload = b"comf" + compressed
|
||||
@@ -262,6 +267,7 @@ class TestIsobmffBrotliExtraction:
|
||||
path.write_bytes(data)
|
||||
|
||||
result = ExifUtils._load_structured_metadata(str(path))
|
||||
assert result["prompt"] is not None
|
||||
assert json.loads(result["prompt"]) == {"text": "hello", "negative": "bad"}
|
||||
|
||||
def test_extract_workflow_as_list(self, tmp_path):
|
||||
@@ -272,6 +278,7 @@ class TestIsobmffBrotliExtraction:
|
||||
path.write_bytes(data)
|
||||
|
||||
result = ExifUtils._load_structured_metadata(str(path))
|
||||
assert result["workflow"] is not None
|
||||
assert json.loads(result["workflow"]) == [{"id": 1}, {"id": 2}]
|
||||
|
||||
def test_over_decompressed_size_limit(self, tmp_path, monkeypatch):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for model sub_type field refactoring."""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from py.utils.models import (
|
||||
BaseModelMetadata,
|
||||
@@ -44,7 +46,7 @@ class TestCheckpointMetadataSubType:
|
||||
|
||||
def test_checkpoint_from_civitai_info_uses_sub_type(self):
|
||||
"""from_civitai_info should use sub_type from version_info."""
|
||||
version_info = {
|
||||
version_info: Dict[str, Any] = {
|
||||
"baseModel": "SDXL",
|
||||
"model": {"name": "Test", "description": "", "tags": []},
|
||||
"files": [{"name": "model.safetensors", "sizeKB": 1000, "hashes": {"SHA256": "abc123"}, "primary": True}],
|
||||
@@ -79,7 +81,7 @@ class TestEmbeddingMetadataSubType:
|
||||
|
||||
def test_embedding_from_civitai_info_uses_sub_type(self):
|
||||
"""from_civitai_info should use sub_type from version_info."""
|
||||
version_info = {
|
||||
version_info: Dict[str, Any] = {
|
||||
"baseModel": "SD1.5",
|
||||
"model": {"name": "Test", "description": "", "tags": []},
|
||||
"files": [{"name": "model.pt", "sizeKB": 1000, "hashes": {"SHA256": "abc123"}, "primary": True}],
|
||||
@@ -113,7 +115,7 @@ class TestLoraMetadataConsistency:
|
||||
|
||||
def test_lora_from_civitai_info_extracts_type(self):
|
||||
"""from_civitai_info should extract type from civitai data."""
|
||||
version_info = {
|
||||
version_info: Dict[str, Any] = {
|
||||
"baseModel": "SDXL",
|
||||
"model": {"name": "Test", "description": "", "tags": [], "type": "Lora"},
|
||||
"files": [{"name": "model.safetensors", "sizeKB": 1000, "hashes": {"SHA256": "abc123"}, "primary": True}],
|
||||
|
||||
@@ -12,6 +12,7 @@ def test_select_preview_returns_first_when_blur_disabled():
|
||||
|
||||
selected, level = select_preview_media(images, blur_mature_content=False)
|
||||
|
||||
assert selected is not None
|
||||
assert selected["url"] == "nsfw"
|
||||
assert level == 32
|
||||
|
||||
@@ -40,6 +41,7 @@ def test_select_preview_respects_configurable_threshold(threshold_name, expected
|
||||
mature_threshold=NSFW_LEVELS[threshold_name],
|
||||
)
|
||||
|
||||
assert selected is not None
|
||||
assert selected["url"] == expected_url
|
||||
assert level == next(item["nsfwLevel"] for item in images if item["url"] == expected_url)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ property-based testing to catch edge cases and ensure correctness.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
|
||||
@@ -80,6 +82,8 @@ class TestNormalizePath:
|
||||
@given(st.text(alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-/\\') | st.none())
|
||||
def test_normalize_path_is_idempotent_for_ascii(self, path: str | None):
|
||||
"""Normalizing an already normalized ASCII path should not change it."""
|
||||
if path is None:
|
||||
return
|
||||
normalized = normalize_path(path)
|
||||
renormalized = normalize_path(normalized)
|
||||
assert normalized == renormalized
|
||||
@@ -160,14 +164,14 @@ class TestCalculateRecipeFingerprint:
|
||||
"""Property-based tests for calculate_recipe_fingerprint function."""
|
||||
|
||||
@given(st.lists(st.dictionaries(st.text(), st.text() | st.integers() | st.floats(), min_size=1), min_size=0, max_size=50))
|
||||
def test_fingerprint_is_deterministic(self, loras: list):
|
||||
def test_fingerprint_is_deterministic(self, loras: list[Dict[str, Any]]):
|
||||
"""Same input should always produce same fingerprint."""
|
||||
fp1 = calculate_recipe_fingerprint(loras)
|
||||
fp2 = calculate_recipe_fingerprint(loras)
|
||||
assert fp1 == fp2
|
||||
|
||||
@given(st.lists(st.dictionaries(st.text(), st.text() | st.integers() | st.floats(), min_size=1), min_size=0, max_size=50))
|
||||
def test_fingerprint_returns_string(self, loras: list):
|
||||
def test_fingerprint_returns_string(self, loras: list[Dict[str, Any]]):
|
||||
"""Function should always return a string."""
|
||||
result = calculate_recipe_fingerprint(loras)
|
||||
assert isinstance(result, str)
|
||||
@@ -178,7 +182,7 @@ class TestCalculateRecipeFingerprint:
|
||||
assert result == ""
|
||||
|
||||
@given(st.lists(st.dictionaries(st.text(), st.text() | st.integers() | st.floats(), min_size=1), min_size=1, max_size=10))
|
||||
def test_fingerprint_different_inputs_produce_different_results(self, loras1: list):
|
||||
def test_fingerprint_different_inputs_produce_different_results(self, loras1: list[Dict[str, Any]]):
|
||||
"""Different inputs should generally produce different fingerprints."""
|
||||
# Create a different input by modifying the first LoRA
|
||||
loras2 = loras1.copy()
|
||||
|
||||
Reference in New Issue
Block a user