Files
AnyImage/tests/test_clipboard_image.py
nan 89087fc2e2 feat: Add new Geometry Nodes groups for image relief and shadeless shader
- Implemented the "O Image Relief Plane" Geometry Nodes group to create a fixed-base image relief using depth images and various parameters.
- Created the "O Shadeless" shader node group to display color without scene lighting while preserving shadows and reflections.
- Introduced a preview feature for node groups, allowing for interactive visualization of node arrangements in an HTML format.
- Added a template HTML file for rendering node previews with customizable views and controls.
2026-09-09 21:24:57 +08:00

938 lines
31 KiB
Python

import importlib
import struct
import sys
import tempfile
import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
from blender_test_support import mock_bpy_imports
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
class ClipboardImageTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.previous_addon_modules = {
name: module for name, module in sys.modules.items()
if name == "anyimage" or name.startswith("anyimage.")
}
cls.temporary_directory = tempfile.TemporaryDirectory()
cls.remove_anyimage_modules()
fake_bpy = ModuleType("bpy")
fake_bpy.app = SimpleNamespace(version=(5, 1, 0))
fake_bpy.props = SimpleNamespace(
BoolProperty=lambda **options: options,
CollectionProperty=lambda **options: options,
EnumProperty=lambda **options: options,
FloatProperty=lambda **options: options,
FloatVectorProperty=lambda **options: options,
IntProperty=lambda **options: options,
PointerProperty=lambda **options: options,
StringProperty=lambda **options: options,
)
fake_bpy.types = SimpleNamespace(
AddonPreferences=object,
GizmoGroup=object,
Image=object,
Menu=object,
Operator=object,
Panel=object,
PropertyGroup=object,
Scene=object,
TOPBAR_MT_file_defaults=SimpleNamespace(),
)
fake_bpy.utils = SimpleNamespace()
fake_bpy.context = SimpleNamespace(
window_manager=SimpleNamespace(
keyconfigs=SimpleNamespace(addon=None),
)
)
fake_bpy.data = SimpleNamespace()
cls.bpy_import_patch = mock_bpy_imports(fake_bpy)
cls.bpy_import_patch.start()
cls.addClassCleanup(cls.bpy_import_patch.stop)
cls.fake_bpy = fake_bpy
cls.addon = ModuleType("anyimage")
cls.addon.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]
sys.modules["anyimage"] = cls.addon
operators = ModuleType("anyimage.operators")
operators.__path__ = [str(PROJECT_ROOT / "src" / "anyimage" / "operators")]
sys.modules["anyimage.operators"] = operators
cls.addon.clipboard_image = importlib.import_module(
"anyimage.operators.clipboard_image.operators"
)
cls.actions = importlib.import_module("anyimage.operators.clipboard_image.actions")
cls.clipboard = importlib.import_module("anyimage.operators.clipboard_image.clipboard")
@classmethod
def tearDownClass(cls):
cls.remove_anyimage_modules()
cls.temporary_directory.cleanup()
sys.modules.update(cls.previous_addon_modules)
def test_operators_use_anyimage_prefix_and_plain_properties(self):
clipboard_image = self.addon.clipboard_image
self.assertEqual(
clipboard_image.PasteClipboardImage.bl_idname,
"anyimage.paste_clipboard_image",
)
self.assertEqual(
clipboard_image.TrackNativeCopy.bl_idname,
"anyimage.track_native_copy",
)
for name in clipboard_image.PasteClipboardImage.__annotations__:
self.assertFalse(name.startswith("anyimage_"), name)
self.assertFalse(name.startswith("o_"), name)
def test_registers_and_removes_clipboard_shortcuts(self):
class KeyMapItems:
def __init__(self):
self.items = []
def new(self, operator_id, event_type, value, **modifiers):
item = SimpleNamespace(
idname=operator_id,
type=event_type,
value=value,
**modifiers,
)
self.items.append(item)
return item
def remove(self, item):
self.items.remove(item)
class KeyMaps:
def __init__(self):
self.items = []
def new(self, name, space_type):
keymap = SimpleNamespace(
name=name,
space_type=space_type,
keymap_items=KeyMapItems(),
)
self.items.append(keymap)
return keymap
keymaps = KeyMaps()
window_manager = SimpleNamespace(
keyconfigs=SimpleNamespace(
addon=SimpleNamespace(keymaps=keymaps),
),
)
clipboard_image = self.addon.clipboard_image
with patch.object(
self.fake_bpy.context,
"window_manager",
window_manager,
):
clipboard_image.register_keymaps()
items = [
item
for keymap in keymaps.items
for item in keymap.keymap_items.items
]
self.assertEqual(
[item.idname for item in items],
[
"anyimage.paste_clipboard_image",
"anyimage.track_native_copy",
"anyimage.paste_clipboard_image",
"anyimage.track_native_copy",
],
)
modifier = "oskey" if sys.platform == "darwin" else "ctrl"
self.assertTrue(all(getattr(item, modifier) for item in items))
clipboard_image.unregister_keymaps()
self.assertTrue(
all(not keymap.keymap_items.items for keymap in keymaps.items)
)
def test_routes_supported_contexts(self):
contexts = (
(self.make_context("VIEW_3D"), "PLANE"),
(
self.make_context("VIEW_3D", mode="SCULPT"),
"TOOL_TEXTURE",
),
(
self.make_context("VIEW_3D", mode="PAINT_VERTEX"),
"TOOL_TEXTURE",
),
(
self.make_context("VIEW_3D", mode="PAINT_TEXTURE"),
"TOOL_TEXTURE",
),
(
self.make_context(
"NODE_EDITOR",
tree_type="ShaderNodeTree",
),
"NODE",
),
(
self.make_context(
"NODE_EDITOR",
tree_type="CompositorNodeTree",
),
"NODE",
),
(
self.make_context(
"NODE_EDITOR",
tree_type="GeometryNodeTree",
),
"NODE",
),
)
for context, expected_target in contexts:
with self.subTest(expected_target=expected_target):
self.assertEqual(
self.actions.target_for_context(context),
expected_target,
)
def test_rejects_unsupported_node_tree(self):
context = self.make_context(
"NODE_EDITOR",
tree_type="TextureNodeTree",
)
self.assertIsNone(self.actions.target_for_context(context))
def test_world_shader_uses_environment_texture(self):
world_space = SimpleNamespace(
tree_type="ShaderNodeTree",
shader_type="WORLD",
)
material_space = SimpleNamespace(
tree_type="ShaderNodeTree",
shader_type="OBJECT",
)
self.assertEqual(
self.actions._node_type_for_space(world_space),
"ShaderNodeTexEnvironment",
)
self.assertEqual(
self.actions._node_type_for_space(material_space),
"ShaderNodeTexImage",
)
def test_pinned_world_tree_uses_environment_texture(self):
world_space = SimpleNamespace(
tree_type="ShaderNodeTree",
shader_type="OBJECT",
id=SimpleNamespace(
bl_rna=SimpleNamespace(identifier="World"),
),
)
self.assertEqual(
self.actions._node_type_for_space(world_space),
"ShaderNodeTexEnvironment",
)
def test_node_location_matches_native_ui_scale_and_anchor_offset(self):
view2d = SimpleNamespace(
region_to_view=Mock(return_value=(200.0, 100.0)),
)
context = SimpleNamespace(
region=SimpleNamespace(view2d=view2d),
preferences=SimpleNamespace(
system=SimpleNamespace(ui_scale=2.0),
),
space_data=SimpleNamespace(cursor_location=(1.0, 2.0)),
)
event = SimpleNamespace(mouse_region_x=80, mouse_region_y=60)
location = self.actions._node_location(context, event)
self.assertEqual(location, (85.0, 55.0))
view2d.region_to_view.assert_called_once_with(80, 60)
def test_node_location_without_event_uses_node_cursor(self):
context = SimpleNamespace(
space_data=SimpleNamespace(cursor_location=(12.0, -8.0)),
)
self.assertEqual(
self.actions._node_location(context, None),
(12.0, -8.0),
)
def test_view3d_location_projects_mouse_at_cursor_depth(self):
projected_location = (4.0, 5.0, 6.0)
project = Mock(return_value=projected_location)
bpy_extras = ModuleType("bpy_extras")
view3d_utils = ModuleType("bpy_extras.view3d_utils")
view3d_utils.region_2d_to_location_3d = project
bpy_extras.view3d_utils = view3d_utils
cursor_location = Mock()
context = SimpleNamespace(
scene=SimpleNamespace(
cursor=SimpleNamespace(location=cursor_location),
),
region=object(),
region_data=object(),
)
event = SimpleNamespace(mouse_region_x=120, mouse_region_y=75)
with patch.dict(
sys.modules,
{
"bpy_extras": bpy_extras,
"bpy_extras.view3d_utils": view3d_utils,
},
):
result = self.actions._view3d_location(context, event)
self.assertIs(result, projected_location)
project.assert_called_once_with(
context.region,
context.region_data,
(120, 75),
cursor_location,
)
def test_view3d_location_without_event_copies_cursor_location(self):
copied_location = object()
cursor_location = SimpleNamespace(
copy=Mock(return_value=copied_location),
)
context = SimpleNamespace(
scene=SimpleNamespace(
cursor=SimpleNamespace(location=cursor_location),
),
)
result = self.actions._view3d_location(context)
self.assertIs(result, copied_location)
cursor_location.copy.assert_called_once_with()
def test_preserves_image_aspect_ratio_for_plane(self):
self.assertEqual(
self.actions._plane_half_dimensions(1920, 1080),
(1.0, 0.5625),
)
self.assertEqual(
self.actions._plane_half_dimensions(800, 1200),
(2 / 3, 1.0),
)
def test_plane_import_defaults_to_reference_image(self):
options = (
self.addon.clipboard_image.PasteClipboardImage
.__annotations__["import_as"]
)
self.assertEqual(options["default"], "REFERENCE")
self.assertEqual(
[item[0] for item in options["items"]],
["PLANE", "REFERENCE"],
)
def test_plane_shadeless_is_disabled_by_default(self):
options = (
self.addon.clipboard_image.PasteClipboardImage
.__annotations__["shadeless"]
)
self.assertFalse(options["default"])
annotations = self.addon.clipboard_image.PasteClipboardImage.__annotations__
self.assertNotIn("subdivisions", annotations)
self.assertNotIn("thickness", annotations)
self.assertNotIn("unshaded", annotations)
def test_native_copy_takes_priority_while_clipboard_is_unchanged(self):
clipboard_image = self.addon.clipboard_image
original_token = clipboard_image._native_copy_token
try:
clipboard_image._native_copy_token = 41
with patch.object(
clipboard_image,
"clipboard_change_token",
return_value=41,
):
self.assertTrue(clipboard_image._should_defer_to_native_paste())
finally:
clipboard_image._native_copy_token = original_token
def test_new_system_clipboard_content_restores_image_paste(self):
clipboard_image = self.addon.clipboard_image
original_token = clipboard_image._native_copy_token
try:
clipboard_image._native_copy_token = 41
with patch.object(
clipboard_image,
"clipboard_change_token",
return_value=42,
):
self.assertFalse(clipboard_image._should_defer_to_native_paste())
finally:
clipboard_image._native_copy_token = original_token
def test_native_copy_tracker_passes_through(self):
clipboard_image = self.addon.clipboard_image
original_token = clipboard_image._native_copy_token
try:
with patch.object(
clipboard_image,
"clipboard_change_token",
return_value=73,
):
result = clipboard_image.TrackNativeCopy().execute(None)
self.assertEqual(result, {"PASS_THROUGH"})
self.assertEqual(clipboard_image._native_copy_token, 73)
finally:
clipboard_image._native_copy_token = original_token
def test_plane_paste_uses_shared_material_for_both_shading_modes(self):
from anyimage.operators.convert_to_plane import image_plane
image = SimpleNamespace(name="Clipboard", size=(16, 8))
context = SimpleNamespace(mode="OBJECT", scene=object())
for shadeless in (False, True):
with (
patch.object(self.actions, "create_image_material", return_value="Material") as create_material,
patch.object(image_plane, "create_image_plane_object") as create_plane,
patch.object(self.actions, "_place_in_view"),
patch.object(self.actions, "_select_only"),
):
self.actions.add_image_plane(context, image, shadeless=shadeless)
create_material.assert_called_once_with(
image, image, shadeless=shadeless, scene=context.scene,
)
self.assertEqual(create_plane.call_args.args[3], "Material")
def test_reference_import_routes_to_image_empty(self):
context = self.make_context("VIEW_3D")
image = object()
expected_reference = object()
expected_location = (1.0, 2.0, 3.0)
original_add_reference_image = self.actions.add_reference_image
self.actions.add_reference_image = (
lambda received_context, received_image, location=None: expected_reference
if (
received_context is context
and received_image is image
and location == expected_location
)
else None
)
try:
result = self.actions.paste_image(
context,
image,
location=expected_location,
import_as="REFERENCE",
)
finally:
self.actions.add_reference_image = original_add_reference_image
self.assertIs(result, expected_reference)
def test_reference_image_uses_drag_and_drop_size(self):
reference = SimpleNamespace(select_set=Mock())
objects = SimpleNamespace(new=Mock(return_value=reference))
context = SimpleNamespace(
mode="OBJECT",
collection=SimpleNamespace(objects=SimpleNamespace(link=Mock())),
selected_objects=(),
view_layer=SimpleNamespace(objects=SimpleNamespace(active=None)),
region_data=None,
)
image = SimpleNamespace(name="Clipboard", size=(1920, 1080))
with patch.object(self.actions.bpy.data, "objects", objects, create=True):
result = self.actions.add_reference_image(
context,
image,
location=(1.0, 2.0, 3.0),
)
self.assertIs(result, reference)
self.assertEqual(reference.empty_display_size, 5.0)
def test_only_import_as_and_shadeless_are_drawn_for_plane_paste(self):
operator = self.addon.clipboard_image.PasteClipboardImage()
drawn_properties = []
operator.layout = SimpleNamespace(
prop=lambda owner, name: drawn_properties.append((owner, name)),
)
operator.paste_target = "PLANE"
operator.import_as = "PLANE"
operator.draw(None)
operator.import_as = "REFERENCE"
operator.draw(None)
operator.paste_target = "NODE"
operator.draw(None)
self.assertEqual(
drawn_properties,
[
(operator, "import_as"),
(operator, "shadeless"),
(operator, "import_as"),
],
)
def test_redo_reuses_captured_node_location(self):
operator = self.addon.clipboard_image.PasteClipboardImage()
operator.paste_target = "NODE"
operator.location = (10.0, 20.0, 0.0)
operator.location_set = True
operator.import_as = "REFERENCE"
operator.shadeless = False
operator.report = Mock()
context = self.make_context(
"NODE_EDITOR",
tree_type="ShaderNodeTree",
)
image = SimpleNamespace(name="Clipboard Image")
clipboard_image = self.addon.clipboard_image
with (
patch.object(
clipboard_image,
"_should_defer_to_native_paste",
return_value=False,
),
patch.object(
clipboard_image,
"read_clipboard_image",
return_value=(b"image", ".png"),
),
patch.object(
clipboard_image,
"acquire_packed_image",
return_value=(image, True),
),
patch.object(clipboard_image, "paste_image") as paste_image,
):
result = operator.execute(context)
self.assertEqual(result, {"FINISHED"})
paste_image.assert_called_once_with(
context,
image,
None,
location=(10.0, 20.0),
import_as="REFERENCE",
shadeless=False,
)
def test_image_plane_requires_object_mode(self):
context = SimpleNamespace(mode="EDIT_MESH")
image = SimpleNamespace(size=(100, 100))
with self.assertRaisesRegex(
self.actions.PasteTargetError,
"Object Mode",
):
self.actions.add_image_plane(context, image)
def test_stencil_dimensions_preserve_scale_and_image_aspect(self):
self.assertEqual(
self.actions._stencil_dimensions(
(1920, 1080),
(400.0, 200.0),
),
(400.0, 225.0),
)
self.assertEqual(
self.actions._stencil_dimensions(
(800, 1200),
(128.0, 256.0),
),
(256.0 * 2 / 3, 256.0),
)
def test_finds_existing_packed_clipboard_image(self):
matching_image = FakeImage(
self.actions.CLIPBOARD_HASH_PROPERTY,
"matching-hash",
packed=True,
)
unpacked_image = FakeImage(
self.actions.CLIPBOARD_HASH_PROPERTY,
"unpacked-hash",
packed=False,
)
original_data = self.actions.bpy.data
self.actions.bpy.data = SimpleNamespace(
images=[matching_image, unpacked_image],
)
try:
self.assertIs(
self.actions._find_packed_clipboard_image("matching-hash"),
matching_image,
)
self.assertIsNone(
self.actions._find_packed_clipboard_image("unpacked-hash")
)
finally:
self.actions.bpy.data = original_data
def test_names_new_packed_image_clipboard(self):
image = FakeLoadedImage()
images = FakeLoadedImages(image)
original_data = self.actions.bpy.data
self.actions.bpy.data = SimpleNamespace(images=images)
try:
loaded_image, reused = self.actions.acquire_packed_image(
b"image-data",
".png",
)
finally:
self.actions.bpy.data = original_data
self.assertIs(loaded_image, image)
self.assertFalse(reused)
self.assertEqual(image.name, "Clipboard")
self.assertTrue(image.packed)
def test_sets_and_reuses_stencil_brush_texture(self):
textures = FakeTextures()
original_data = self.actions.bpy.data
self.actions.bpy.data = SimpleNamespace(textures=textures)
brush = SimpleNamespace(
is_editable=True,
texture=None,
texture_slot=SimpleNamespace(
texture=None,
map_mode="VIEW_PLANE",
),
stencil_dimension=(256.0, 256.0),
update_tag=lambda: None,
)
context = SimpleNamespace(brush=brush)
image = SimpleNamespace(name="Clipboard Image", size=(1920, 1080))
try:
first_texture = self.actions.set_tool_texture(context, image)
second_texture = self.actions.set_tool_texture(context, image)
finally:
self.actions.bpy.data = original_data
self.assertIs(first_texture, second_texture)
self.assertEqual(len(textures), 1)
self.assertIs(brush.texture, first_texture)
self.assertIs(brush.texture_slot.texture, first_texture)
self.assertEqual(brush.texture_slot.map_mode, "STENCIL")
self.assertEqual(brush.stencil_dimension, (256.0, 144.0))
def test_localizes_read_only_active_brush(self):
local_brush = SimpleNamespace(is_editable=True, library=None)
linked_brush = SimpleNamespace(
is_editable=False,
library=None,
make_local=lambda: local_brush,
)
self.assertIs(
self.actions._ensure_editable_brush(
SimpleNamespace(),
linked_brush,
),
local_brush,
)
def test_copies_and_activates_runtime_library_brush(self):
local_brush = SimpleNamespace(
name="Paint Hard Clipboard",
is_editable=True,
library=None,
)
library_brush = SimpleNamespace(
name="Paint Hard",
is_editable=True,
library=object(),
)
paint_settings = SimpleNamespace(brush=library_brush)
context = SimpleNamespace(
mode="SCULPT",
tool_settings=SimpleNamespace(sculpt=paint_settings),
)
original_ops = getattr(self.actions.bpy, "ops", None)
def save_brush_as(**options):
self.assertEqual(
options["asset_library_reference"],
"LOCAL",
)
self.assertEqual(options["name"], "Paint Hard Clipboard")
paint_settings.brush = local_brush
return {"FINISHED"}
self.actions.bpy.ops = SimpleNamespace(
brush=SimpleNamespace(asset_save_as=save_brush_as),
)
try:
result = self.actions._ensure_editable_brush(
context,
library_brush,
)
finally:
if original_ops is None:
del self.actions.bpy.ops
else:
self.actions.bpy.ops = original_ops
self.assertIs(result, local_brush)
def test_active_brush_prefers_current_mode_settings(self):
correct_brush = object()
context = SimpleNamespace(
brush=object(),
mode="SCULPT",
tool_settings=SimpleNamespace(
sculpt=SimpleNamespace(brush=correct_brush),
),
)
self.assertIs(self.actions._active_brush(context), correct_brush)
def test_vertex_and_texture_paint_use_unbiased_white(self):
for mode in ("PAINT_VERTEX", "PAINT_TEXTURE"):
with self.subTest(mode=mode):
brush = SimpleNamespace(color=(0.2, 0.4, 0.8))
unified_settings = SimpleNamespace(color=(0.8, 0.4, 0.2))
context = SimpleNamespace(
mode=mode,
tool_settings=SimpleNamespace(
**{
self.actions.PAINT_SETTINGS_BY_MODE[mode][0]:
SimpleNamespace(
unified_paint_settings=unified_settings,
)
},
),
)
self.actions._set_unbiased_paint_color(context, brush)
self.assertEqual(brush.color, (1.0, 1.0, 1.0))
self.assertEqual(
unified_settings.color,
(1.0, 1.0, 1.0),
)
def test_sculpt_keeps_brush_color(self):
brush = SimpleNamespace(color=(0.2, 0.4, 0.8))
self.actions._set_unbiased_paint_color(
SimpleNamespace(mode="SCULPT"),
brush,
)
self.assertEqual(brush.color, (0.2, 0.4, 0.8))
def test_wraps_info_dib_as_bmp(self):
dib_header = struct.pack(
"<IiiHHIIiiII",
40,
1,
1,
1,
24,
0,
4,
0,
0,
0,
0,
)
dib_data = dib_header + b"\x00\x00\xff\x00"
bmp_data = self.clipboard.dib_to_bmp(dib_data)
signature, file_size, _, _, pixel_offset = struct.unpack_from(
"<2sIHHI",
bmp_data,
)
self.assertEqual(signature, b"BM")
self.assertEqual(file_size, len(bmp_data))
self.assertEqual(pixel_offset, 54)
self.assertEqual(bmp_data[14:], dib_data)
def test_rejects_truncated_dib(self):
with self.assertRaises(self.clipboard.ClipboardImageError):
self.clipboard.dib_to_bmp(b"\x28\x00\x00\x00")
def test_reads_macos_clipboard_image_as_png(self):
def export_image(_osascript, output_file):
output_file.write_bytes(b"png image")
with patch.object(
self.clipboard.shutil,
"which",
return_value="/usr/bin/osascript",
), patch.object(
self.clipboard,
"_export_macos_clipboard_image",
side_effect=export_image,
):
image_data, suffix = self.clipboard._read_macos_clipboard_image()
self.assertEqual(image_data, b"png image")
self.assertEqual(suffix, ".png")
def test_macos_clipboard_without_image_is_unavailable(self):
with patch.object(
self.clipboard.shutil,
"which",
return_value="/usr/bin/osascript",
), patch.object(
self.clipboard,
"_export_macos_clipboard_image",
):
with self.assertRaises(self.clipboard.ClipboardImageUnavailable):
self.clipboard._read_macos_clipboard_image()
def test_linux_clipboard_prefers_wayland_png(self):
def find_command(command_name):
if command_name == "wl-paste":
return "/usr/bin/wl-paste"
return None
with patch.object(
self.clipboard.shutil,
"which",
side_effect=find_command,
), patch.object(
self.clipboard,
"_run_image_command",
return_value=b"png image",
) as run_command:
image_data, suffix = self.clipboard._read_linux_clipboard_image()
self.assertEqual(image_data, b"png image")
self.assertEqual(suffix, ".png")
run_command.assert_called_once_with(
[
"/usr/bin/wl-paste",
"--no-newline",
"--type",
"image/png",
]
)
def test_linux_clipboard_falls_back_to_xclip(self):
def find_command(command_name):
if command_name == "xclip":
return "/usr/bin/xclip"
return None
with patch.object(
self.clipboard.shutil,
"which",
side_effect=find_command,
), patch.object(
self.clipboard,
"_run_image_command",
return_value=b"png image",
) as run_command:
image_data, suffix = self.clipboard._read_linux_clipboard_image()
self.assertEqual(image_data, b"png image")
self.assertEqual(suffix, ".png")
run_command.assert_called_once_with(
[
"/usr/bin/xclip",
"-selection",
"clipboard",
"-t",
"image/png",
"-o",
]
)
def test_linux_clipboard_requires_supported_command(self):
with patch.object(
self.clipboard.shutil,
"which",
return_value=None,
):
with self.assertRaises(self.clipboard.ClipboardImageUnavailable):
self.clipboard._read_linux_clipboard_image()
@staticmethod
def make_context(area_type, mode="OBJECT", **space_options):
return SimpleNamespace(
area=SimpleNamespace(type=area_type),
mode=mode,
region=SimpleNamespace(type="WINDOW"),
space_data=SimpleNamespace(**space_options),
)
@staticmethod
def remove_anyimage_modules():
for module_name in tuple(sys.modules):
if module_name == "anyimage" or module_name.startswith("anyimage."):
sys.modules.pop(module_name)
class FakeImage:
def __init__(self, property_name, image_hash, packed):
self.properties = {property_name: image_hash}
self.packed_file = object() if packed else None
self.packed_files = ()
def get(self, property_name, default=None):
return self.properties.get(property_name, default)
class FakeLoadedImage(dict):
def __init__(self):
super().__init__()
self.name = None
self.packed = False
def pack(self):
self.packed = True
class FakeLoadedImages(list):
def __init__(self, loaded_image):
super().__init__()
self.loaded_image = loaded_image
def load(self, _path, check_existing):
return self.loaded_image
def remove(self, image):
super().remove(image)
class FakeTexture(dict):
def __init__(self, name, texture_type):
super().__init__()
self.name = name
self.type = texture_type
self.image = None
class FakeTextures(list):
def new(self, name, type):
texture = FakeTexture(name, type)
self.append(texture)
return texture
def remove(self, texture):
super().remove(texture)
if __name__ == "__main__":
unittest.main()