Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
.tox/
.nox/

# Claude settings
.claude/*

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDEs and editors
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
Pipfile.lock

# Celery
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# Temporary files
*.tmp
*.temp
.tmp/
temp/

# Logs
*.log
logs/
283 changes: 283 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
[tool.poetry]
name = "extra-image-list"
version = "0.2.9"
description = "An alternative image list for UV/Image Editor in Blender"
authors = ["Miki (meshlogic) <meshlogic@example.com>", "Rombout Versluijs <rombout@example.com>"]
readme = "README.md"
license = "GPL-3.0"
homepage = "https://github.com/schroef/Extra-Image-List"
repository = "https://github.com/schroef/Extra-Image-List"
keywords = ["blender", "addon", "uv", "image", "editor"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3",
"Topic :: Multimedia :: Graphics :: 3D Modeling",
"Topic :: Software Development :: Libraries :: Python Modules",
]
packages = [{include = "*.py", from = "."}]
package-mode = false

[tool.poetry.dependencies]
python = "^3.7"

[tool.poetry.group.test.dependencies]
pytest = "^7.0.0"
pytest-cov = "^4.0.0"
pytest-mock = "^3.10.0"

# Using group dependencies for testing instead of scripts
# Run tests with: poetry run pytest

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--verbose",
"--cov=.",
"--cov-report=term-missing",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=80",
]
markers = [
"unit: marks tests as unit tests (fast, isolated)",
"integration: marks tests as integration tests (slower, with dependencies)",
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
]

[tool.coverage.run]
source = ["."]
omit = [
"tests/*",
"htmlcov/*",
".venv/*",
"venv/*",
"*.egg-info/*",
"build/*",
"dist/*",
".pytest_cache/*",
".coverage",
"setup.py",
"__init__.py",
]
branch = true

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
show_missing = true
precision = 2

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"
Empty file added tests/__init__.py
Empty file.
173 changes: 173 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import os
import tempfile
import pytest
from pathlib import Path
from unittest.mock import Mock, MagicMock


@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests that gets cleaned up automatically."""
with tempfile.TemporaryDirectory() as tmp_dir:
yield Path(tmp_dir)


@pytest.fixture
def temp_file():
"""Create a temporary file for tests that gets cleaned up automatically."""
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_file:
yield Path(tmp_file.name)

# Clean up the file after the test
try:
os.unlink(tmp_file.name)
except FileNotFoundError:
pass


@pytest.fixture
def mock_bpy():
"""Mock the bpy module for testing Blender addon functionality."""
mock_bpy = MagicMock()

# Mock common bpy structures
mock_bpy.data.images = []
mock_bpy.types.Scene = MagicMock()
mock_bpy.props.EnumProperty = MagicMock()
mock_bpy.props.StringProperty = MagicMock()
mock_bpy.props.BoolProperty = MagicMock()
mock_bpy.props.IntProperty = MagicMock()
mock_bpy.props.PointerProperty = MagicMock()
mock_bpy.utils.register_class = MagicMock()
mock_bpy.utils.unregister_class = MagicMock()
mock_bpy.context.scene = MagicMock()
mock_bpy.context.space_data = MagicMock()
mock_bpy.context.active_object = MagicMock()
mock_bpy.app.handlers.depsgraph_update_post = MagicMock()

return mock_bpy


@pytest.fixture
def mock_blender_context():
"""Mock Blender context for testing."""
context = MagicMock()
context.scene = MagicMock()
context.space_data = MagicMock()
context.space_data.image = None
context.active_object = MagicMock()
context.active_object.type = 'MESH'
context.active_object.active_material = MagicMock()
context.screen.areas = []

return context


@pytest.fixture
def mock_image():
"""Mock Blender image datablock for testing."""
image = MagicMock()
image.name = "test_image.png"
image.filepath = "/path/to/test_image.png"
image.source = 'FILE'
image.has_data = True
image.users = 1
image.use_fake_user = False
image.packed_file = None
image.size = [1024, 1024]
image.depth = 32

return image


@pytest.fixture
def sample_image_list():
"""Create a sample list of mock images for testing."""
images = []
for i in range(5):
image = MagicMock()
image.name = f"image_{i}.png"
image.filepath = f"/path/to/image_{i}.png"
image.source = 'FILE'
image.has_data = True
image.users = 1 if i < 3 else 0 # First 3 have users, last 2 don't
image.use_fake_user = i == 4 # Last one is fake user
image.packed_file = None if i != 2 else MagicMock() # Third one is packed
image.size = [512 * (i + 1), 512 * (i + 1)]
image.depth = 24
images.append(image)

return images


@pytest.fixture
def mock_addon_preferences():
"""Mock addon preferences for testing."""
prefs = MagicMock()
prefs.style = 'PREVIEW'
prefs.rows = 4
prefs.cols = 6
prefs.clean_enabled = False
prefs.clear_mode = 'NO USERS'
prefs.info = False
prefs.image_id = 0

return prefs


@pytest.fixture(autouse=True)
def reset_environment():
"""Reset environment variables and state before each test."""
# Store original environment
original_env = dict(os.environ)

yield

# Restore original environment
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def sample_config():
"""Provide sample configuration data for tests."""
return {
"preview_rows": 4,
"preview_cols": 6,
"default_style": "PREVIEW",
"clean_enabled": False,
"show_info": True,
}


class MockBlenderOperator:
"""Mock Blender operator for testing."""

def __init__(self):
self.bl_idname = "test.operator"
self.bl_label = "Test Operator"
self.bl_description = "Test operator description"

def execute(self, context):
return {'FINISHED'}

@classmethod
def poll(cls, context):
return True


@pytest.fixture
def mock_operator():
"""Provide a mock Blender operator for testing."""
return MockBlenderOperator()


@pytest.fixture
def isolated_filesystem(temp_dir):
"""Change to a temporary directory for the test and restore afterwards."""
original_cwd = os.getcwd()
os.chdir(temp_dir)

yield temp_dir

os.chdir(original_cwd)
Empty file added tests/integration/__init__.py
Empty file.
Loading