Add comprehensive tests for edge depth extension, empty image edits, and material view adaptation
- Introduced `test_edge_depth.py` to validate the functionality of edge depth extension, ensuring core alpha preservation, proper depth handling, and edge padding behavior. - Created `test_empty_image_edits.py` to verify the behavior of image edits in Blender, focusing on shared edits, preservation of image properties, and handling of failed operations. - Developed `test_material_view_adaptation.py` to test material adaptation preferences, ensuring color space and alpha settings are correctly applied and preserved across various scenarios.
This commit is contained in:
151
tools/pack.py
151
tools/pack.py
@@ -1,3 +1,7 @@
|
||||
import argparse
|
||||
import platform as host_platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -10,7 +14,12 @@ from zipfile import ZIP_DEFLATED, ZipFile
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_DIR = PROJECT_ROOT / "src" / "anyimage"
|
||||
DIST_DIR = PROJECT_ROOT / "dist"
|
||||
PYTHON_VERSION = "3.11"
|
||||
PYTHON_TARGETS = {
|
||||
"3.11": "numpy>=1.26,<2.0",
|
||||
"3.13": "numpy>=2.1,<3.0",
|
||||
}
|
||||
# Resolve NumPy for dependency compatibility, but use Blender's runtime copy.
|
||||
BLENDER_PROVIDED_PACKAGES = frozenset({"numpy"})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -40,29 +49,102 @@ def download_wheels(platform, destination):
|
||||
if not dependencies:
|
||||
return []
|
||||
config = PLATFORM_CONFIGS[platform]
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"download",
|
||||
*dependencies,
|
||||
"--dest",
|
||||
str(destination),
|
||||
"--find-links",
|
||||
str(SOURCE_DIR / "wheels"),
|
||||
"--only-binary=:all:",
|
||||
f"--python-version={PYTHON_VERSION}",
|
||||
f"--platform={config.pip_platform}",
|
||||
# Blender supplies NumPy; resolve against each runtime's compatible range.
|
||||
dependencies = [
|
||||
dependency
|
||||
for dependency in dependencies
|
||||
if re.split(r"[\s\[<>=!~;@]", dependency, maxsplit=1)[0]
|
||||
.lower().replace("_", "-") not in BLENDER_PROVIDED_PACKAGES
|
||||
]
|
||||
subprocess.run(command, check=True)
|
||||
wheels = sorted(destination.glob("*.whl"))
|
||||
if not wheels:
|
||||
raise RuntimeError(f"No wheels downloaded for {platform}")
|
||||
return wheels
|
||||
wheels_by_name = {}
|
||||
shared_constraints = destination / "shared-constraints.txt"
|
||||
for python_version, numpy_requirement in PYTHON_TARGETS.items():
|
||||
target_directory = destination / python_version
|
||||
target_directory.mkdir()
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"download",
|
||||
*dependencies,
|
||||
numpy_requirement,
|
||||
"--dest",
|
||||
str(target_directory),
|
||||
"--find-links",
|
||||
str(SOURCE_DIR / "wheels"),
|
||||
"--only-binary=:all:",
|
||||
f"--python-version={python_version}",
|
||||
"--implementation=cp",
|
||||
f"--platform={config.pip_platform}",
|
||||
]
|
||||
if shared_constraints.exists():
|
||||
command.extend(["--constraint", str(shared_constraints)])
|
||||
subprocess.run(command, check=True)
|
||||
wheels = extension_wheels(sorted(target_directory.glob("*.whl")))
|
||||
if not wheels:
|
||||
raise RuntimeError(
|
||||
f"No wheels downloaded for {platform}, Python {python_version}"
|
||||
)
|
||||
if not shared_constraints.exists():
|
||||
shared_constraints.write_text(
|
||||
"\n".join(
|
||||
"==".join(wheel.name.split("-")[:2])
|
||||
for wheel in wheels
|
||||
if wheel.name.endswith("-none-any.whl")
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
for wheel in wheels:
|
||||
wheels_by_name[wheel.name] = wheel
|
||||
return [wheels_by_name[name] for name in sorted(wheels_by_name)]
|
||||
|
||||
|
||||
def extension_wheels(wheels):
|
||||
"""Exclude distributions supplied by the supported Blender runtime."""
|
||||
return [
|
||||
wheel
|
||||
for wheel in wheels
|
||||
if wheel.name.split("-", 1)[0].lower().replace("_", "-")
|
||||
not in BLENDER_PROVIDED_PACKAGES
|
||||
]
|
||||
|
||||
|
||||
def current_platform():
|
||||
target = {
|
||||
("Windows", "amd64"): "windows-x64",
|
||||
("Darwin", "arm64"): "macos-arm64",
|
||||
("Linux", "x86_64"): "linux-x64",
|
||||
}.get((host_platform.system(), host_platform.machine().lower()))
|
||||
if target is None:
|
||||
raise ValueError("Unsupported host platform; specify --platform explicitly")
|
||||
return target
|
||||
|
||||
|
||||
def prepare_local_dependencies(platform):
|
||||
"""Synchronize local wheels and Manifest after a successful download."""
|
||||
if platform not in PLATFORMS:
|
||||
raise ValueError(f"Unsupported platform: {platform}")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
wheels = download_wheels(platform, Path(directory))
|
||||
manifest = manifest_for_platform(platform, wheels)
|
||||
destination = SOURCE_DIR / "wheels"
|
||||
destination.mkdir(exist_ok=True)
|
||||
for wheel in wheels:
|
||||
shutil.copy2(wheel, destination / wheel.name)
|
||||
selected = {wheel.name for wheel in wheels}
|
||||
for wheel in destination.glob("*.whl"):
|
||||
if wheel.name not in selected:
|
||||
wheel.unlink()
|
||||
manifest_path = SOURCE_DIR / "blender_manifest.toml"
|
||||
temporary_manifest = manifest_path.with_suffix(".toml.tmp")
|
||||
temporary_manifest.write_text(manifest, encoding="utf-8")
|
||||
temporary_manifest.replace(manifest_path)
|
||||
return sorted(destination.glob("*.whl"))
|
||||
|
||||
|
||||
def manifest_for_platform(platform, wheels):
|
||||
"""Return manifest text for one platform and its downloaded wheels."""
|
||||
wheels = extension_wheels(wheels)
|
||||
text = (SOURCE_DIR / "blender_manifest.toml").read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
table_start = next(
|
||||
@@ -85,14 +167,13 @@ def manifest_for_platform(platform, wheels):
|
||||
len(head),
|
||||
)
|
||||
head.insert(insert_at, f'platforms = ["{platform}"]')
|
||||
wheel_names = ", ".join(
|
||||
f'"./wheels/{wheel.name}"' for wheel in wheels
|
||||
)
|
||||
wheel_names = ", ".join(f'"./wheels/{wheel.name}"' for wheel in wheels)
|
||||
head.insert(insert_at + 1, f"wheels = [{wheel_names}]")
|
||||
return "\n".join(head + tail) + "\n"
|
||||
|
||||
|
||||
def _build_extension(platform, wheels):
|
||||
wheels = extension_wheels(wheels)
|
||||
manifest_text = manifest_for_platform(platform, wheels)
|
||||
manifest = tomllib.loads(manifest_text)
|
||||
output_path = DIST_DIR / (
|
||||
@@ -134,8 +215,30 @@ def build_all():
|
||||
return [build_extension(platform) for platform in PLATFORMS]
|
||||
|
||||
|
||||
def main():
|
||||
extension_paths = build_all()
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Prepare local dependencies or pack Blender extensions"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--prepare",
|
||||
action="store_true",
|
||||
help="Update source wheels and Manifest for local testing",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--platform",
|
||||
choices=PLATFORMS,
|
||||
help="Target platform; preparation defaults to this computer",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
if args.prepare:
|
||||
platform = args.platform or current_platform()
|
||||
wheels = prepare_local_dependencies(platform)
|
||||
print(
|
||||
f"Prepared {len(wheels)} wheels for {platform} in {SOURCE_DIR / 'wheels'}"
|
||||
)
|
||||
print(f"Updated {SOURCE_DIR / 'blender_manifest.toml'}")
|
||||
return
|
||||
extension_paths = [build_extension(args.platform)] if args.platform else build_all()
|
||||
for extension_path in extension_paths:
|
||||
size_mib = extension_path.stat().st_size / (1024 * 1024)
|
||||
print(f"Built {extension_path} ({size_mib:.2f} MiB)")
|
||||
|
||||
Reference in New Issue
Block a user