Merge branch 'blender-v4.3-release'
This commit is contained in:
@@ -94,7 +94,7 @@ test: FORCE
|
||||
watch_test: FORCE
|
||||
@cd "$(BASE_DIR)" && \
|
||||
while true; do \
|
||||
$(MAKE) test_cli; \
|
||||
$(MAKE) test; \
|
||||
inotifywait -q -e close_write $(EXTRA_WATCH_FILES) $(PY_FILES) ; \
|
||||
tput clear; \
|
||||
done
|
||||
|
||||
@@ -102,6 +102,7 @@ def manifest_compatible_with_wheel_data_or_error(
|
||||
from bl_pkg.bl_extension_utils import (
|
||||
pkg_manifest_dict_is_valid_or_error,
|
||||
toml_from_filepath,
|
||||
python_versions_from_wheels,
|
||||
)
|
||||
from bl_pkg.bl_extension_ops import (
|
||||
pkg_manifest_params_compatible_or_error_for_this_system,
|
||||
@@ -115,10 +116,24 @@ def manifest_compatible_with_wheel_data_or_error(
|
||||
if (error := pkg_manifest_dict_is_valid_or_error(manifest_dict, from_repo=False, strict=False)):
|
||||
return error
|
||||
|
||||
python_versions = []
|
||||
if (wheel_files := manifest_dict.get("wheels", None)) is not None:
|
||||
if isinstance(python_versions_test := python_versions_from_wheels(wheel_files), str):
|
||||
print("Error parsing wheel versions: {:s} from \"{:s}\"".format(
|
||||
python_versions_test,
|
||||
pkg_manifest_filepath,
|
||||
))
|
||||
else:
|
||||
python_versions = [
|
||||
".".join(str(i) for i in v)
|
||||
for v in python_versions_test
|
||||
]
|
||||
|
||||
if isinstance(error := pkg_manifest_params_compatible_or_error_for_this_system(
|
||||
blender_version_min=manifest_dict.get("blender_version_min", ""),
|
||||
blender_version_max=manifest_dict.get("blender_version_max", ""),
|
||||
platforms=manifest_dict.get("platforms", ""),
|
||||
python_versions=python_versions,
|
||||
), str):
|
||||
return error
|
||||
|
||||
@@ -569,6 +584,7 @@ _repo_cache_store = None
|
||||
def repo_cache_store_ensure():
|
||||
# pylint: disable-next=global-statement
|
||||
global _repo_cache_store
|
||||
import sys
|
||||
|
||||
if _repo_cache_store is not None:
|
||||
return _repo_cache_store
|
||||
@@ -577,7 +593,10 @@ def repo_cache_store_ensure():
|
||||
bl_extension_ops,
|
||||
bl_extension_utils,
|
||||
)
|
||||
_repo_cache_store = bl_extension_utils.RepoCacheStore(bpy.app.version)
|
||||
_repo_cache_store = bl_extension_utils.RepoCacheStore(
|
||||
blender_version=bpy.app.version,
|
||||
python_version=sys.version_info[:3],
|
||||
)
|
||||
bl_extension_ops.repo_cache_store_refresh_from_prefs(_repo_cache_store)
|
||||
return _repo_cache_store
|
||||
|
||||
|
||||
@@ -70,8 +70,13 @@ _ext_base_pkg_idname_with_dot = _ext_base_pkg_idname + "."
|
||||
|
||||
|
||||
def url_append_defaults(url):
|
||||
import sys
|
||||
from .bl_extension_utils import url_append_query_for_blender
|
||||
return url_append_query_for_blender(url, blender_version=bpy.app.version)
|
||||
return url_append_query_for_blender(
|
||||
url=url,
|
||||
blender_version=bpy.app.version,
|
||||
python_version=sys.version_info[:3],
|
||||
)
|
||||
|
||||
|
||||
def url_normalize(url):
|
||||
@@ -546,8 +551,10 @@ def pkg_manifest_params_compatible_or_error_for_this_system(
|
||||
blender_version_min, # `str`
|
||||
blender_version_max, # `str`
|
||||
platforms, # `list[str]`
|
||||
python_versions, # `list[str]`
|
||||
): # `str | None`
|
||||
# Return true if the parameters are compatible with this system.
|
||||
import sys
|
||||
from .bl_extension_utils import (
|
||||
pkg_manifest_params_compatible_or_error,
|
||||
platform_from_this_system,
|
||||
@@ -557,8 +564,10 @@ def pkg_manifest_params_compatible_or_error_for_this_system(
|
||||
blender_version_min=blender_version_min,
|
||||
blender_version_max=blender_version_max,
|
||||
platforms=platforms,
|
||||
python_versions=python_versions,
|
||||
# This system.
|
||||
this_platform=platform_from_this_system(),
|
||||
this_python_version=sys.version_info,
|
||||
this_blender_version=bpy.app.version,
|
||||
error_fn=print,
|
||||
)
|
||||
@@ -913,7 +922,7 @@ def _pkg_marked_by_repo(repo_cache_store, pkg_manifest_all):
|
||||
# Wheel Handling
|
||||
#
|
||||
|
||||
def _extensions_wheel_filter_for_platform(wheels):
|
||||
def _extensions_wheel_filter_for_this_system(wheels):
|
||||
|
||||
# Copied from `wheel.bwheel_dist.get_platform(..)` which isn't part of Python.
|
||||
# This misses some additional checks which aren't supported by official Blender builds,
|
||||
@@ -922,6 +931,13 @@ def _extensions_wheel_filter_for_platform(wheels):
|
||||
import sysconfig
|
||||
platform_tag_current = sysconfig.get_platform().replace("-", "_")
|
||||
|
||||
import sys
|
||||
from .bl_extension_utils import (
|
||||
python_versions_from_wheel_python_tag,
|
||||
)
|
||||
|
||||
python_version_current = sys.version_info[:2]
|
||||
|
||||
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-name-convention
|
||||
# This also defines the name spec:
|
||||
# `{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl`
|
||||
@@ -942,9 +958,10 @@ def _extensions_wheel_filter_for_platform(wheels):
|
||||
if not (5 <= len(wheel_filename_split) <= 6):
|
||||
print("Error: wheel doesn't follow naming spec \"{:s}\"".format(wheel_filename))
|
||||
continue
|
||||
# TODO: Match Python & ABI tags.
|
||||
_python_tag, _abi_tag, platform_tag = wheel_filename_split[-3:]
|
||||
# TODO: Match ABI tags.
|
||||
python_tag, _abi_tag, platform_tag = wheel_filename_split[-3:]
|
||||
|
||||
# Perform Platform Checks.
|
||||
if platform_tag in {"any", platform_tag_current}:
|
||||
pass
|
||||
elif platform_tag_current.startswith("macosx_") and (
|
||||
@@ -973,7 +990,35 @@ def _extensions_wheel_filter_for_platform(wheels):
|
||||
)
|
||||
continue
|
||||
|
||||
# Perform Python Version Checks.
|
||||
if isinstance(python_versions := python_versions_from_wheel_python_tag(python_tag), str):
|
||||
print("Error: wheel \"{:s}\" unable to parse Python version {:s}".format(wheel_filename, python_versions))
|
||||
else:
|
||||
python_version_is_compat = False
|
||||
for python_version in python_versions:
|
||||
if len(python_version) == 1:
|
||||
if python_version_current[0] == python_version[0]:
|
||||
python_version_is_compat = True
|
||||
break
|
||||
if python_version_current == python_version:
|
||||
python_version_is_compat = True
|
||||
break
|
||||
if not python_version_is_compat:
|
||||
# Useful to know, can quiet print in the future.
|
||||
print(
|
||||
"Skipping wheel for other Python version",
|
||||
"({:s}=>({:s}) not in {:d}.{:d}):".format(
|
||||
python_tag,
|
||||
", ".join([".".join(str(i) for i in v) for v in python_versions]),
|
||||
python_version_current[0],
|
||||
python_version_current[1],
|
||||
),
|
||||
wheel_filename,
|
||||
)
|
||||
continue
|
||||
|
||||
wheels_compatible.append(wheel)
|
||||
|
||||
return wheels_compatible
|
||||
|
||||
|
||||
@@ -984,7 +1029,7 @@ def pkg_wheel_filter(
|
||||
wheels_rel, # `list[str]`
|
||||
): # `-> tuple[str, list[str]]`
|
||||
# Filter only the wheels for this platform.
|
||||
wheels_rel = _extensions_wheel_filter_for_platform(wheels_rel)
|
||||
wheels_rel = _extensions_wheel_filter_for_this_system(wheels_rel)
|
||||
if not wheels_rel:
|
||||
return None
|
||||
|
||||
@@ -1884,6 +1929,7 @@ class EXTENSIONS_OT_package_upgrade_all(Operator, _ExtCmdMixIn):
|
||||
return "" # Default.
|
||||
|
||||
def exec_command_iter(self, is_modal):
|
||||
import sys
|
||||
from . import bl_extension_utils
|
||||
# pylint: disable-next=attribute-defined-outside-init
|
||||
self._repo_directories = set()
|
||||
@@ -1974,6 +2020,7 @@ class EXTENSIONS_OT_package_upgrade_all(Operator, _ExtCmdMixIn):
|
||||
pkg_id_sequence=pkg_id_sequence_iter,
|
||||
online_user_agent=online_user_agent_from_blender(),
|
||||
blender_version=bpy.app.version,
|
||||
python_version=sys.version_info[:3],
|
||||
access_token=repo_item.access_token,
|
||||
timeout=prefs.system.network_timeout,
|
||||
use_cache=repo_item.use_cache,
|
||||
@@ -2055,6 +2102,7 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
|
||||
enable_on_install: rna_prop_enable_on_install
|
||||
|
||||
def exec_command_iter(self, is_modal):
|
||||
import sys
|
||||
from . import bl_extension_utils
|
||||
|
||||
repos_all = extension_repos_read()
|
||||
@@ -2096,6 +2144,7 @@ class EXTENSIONS_OT_package_install_marked(Operator, _ExtCmdMixIn):
|
||||
pkg_id_sequence=pkg_id_sequence_iter,
|
||||
online_user_agent=online_user_agent_from_blender(),
|
||||
blender_version=bpy.app.version,
|
||||
python_version=sys.version_info[:3],
|
||||
access_token=repo_item.access_token,
|
||||
timeout=prefs.system.network_timeout,
|
||||
use_cache=repo_item.use_cache,
|
||||
@@ -2383,6 +2432,7 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
|
||||
url: rna_prop_url
|
||||
|
||||
def exec_command_iter(self, is_modal):
|
||||
import sys
|
||||
from . import bl_extension_utils
|
||||
from .bl_extension_utils import (
|
||||
pkg_manifest_dict_from_archive_or_error,
|
||||
@@ -2497,6 +2547,7 @@ class EXTENSIONS_OT_package_install_files(Operator, _ExtCmdMixIn):
|
||||
directory=directory,
|
||||
files=pkg_files,
|
||||
blender_version=bpy.app.version,
|
||||
python_version=sys.version_info[:3],
|
||||
use_idle=is_modal,
|
||||
python_args=bpy.app.python_args,
|
||||
)
|
||||
@@ -2825,6 +2876,7 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
|
||||
return True
|
||||
|
||||
def exec_command_iter(self, is_modal):
|
||||
import sys
|
||||
from . import bl_extension_utils
|
||||
|
||||
if not self._is_ready_to_execute():
|
||||
@@ -2879,6 +2931,7 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
|
||||
pkg_id_sequence=(pkg_id,),
|
||||
online_user_agent=online_user_agent_from_blender(),
|
||||
blender_version=bpy.app.version,
|
||||
python_version=sys.version_info[:3],
|
||||
access_token=repo_item.access_token,
|
||||
timeout=prefs.system.network_timeout,
|
||||
use_cache=repo_item.use_cache,
|
||||
@@ -3006,6 +3059,10 @@ class EXTENSIONS_OT_package_install(Operator, _ExtCmdMixIn):
|
||||
blender_version_min=url_params.get("blender_version_min", ""),
|
||||
blender_version_max=url_params.get("blender_version_max", ""),
|
||||
platforms=[platform for platform in url_params.get("platforms", "").split(",") if platform],
|
||||
python_versions=[
|
||||
python_version for python_version in url_params.get("python_versions", "").split(",")
|
||||
if python_version
|
||||
],
|
||||
), str):
|
||||
self.report({'ERROR'}, iface_("The extension is incompatible with this system:\n{:s}").format(error))
|
||||
return {'CANCELLED'}
|
||||
|
||||
@@ -43,6 +43,8 @@ __all__ = (
|
||||
"pkg_manifest_dict_from_archive_or_error",
|
||||
"pkg_manifest_archive_url_abs_from_remote_url",
|
||||
|
||||
"python_versions_from_wheel_python_tag",
|
||||
|
||||
"CommandBatch",
|
||||
"RepoCacheStore",
|
||||
|
||||
@@ -431,7 +433,12 @@ def _url_append_query(url: str, query: dict[str, str]) -> str:
|
||||
return new_url
|
||||
|
||||
|
||||
def url_append_query_for_blender(url: str, blender_version: tuple[int, int, int]) -> str:
|
||||
def url_append_query_for_blender(
|
||||
*,
|
||||
url: str,
|
||||
blender_version: tuple[int, int, int],
|
||||
python_version: tuple[int, int, int],
|
||||
) -> str:
|
||||
# `blender_version` is typically `bpy.app.version`.
|
||||
|
||||
# While this won't cause errors, it's redundant to add this information to file URL's.
|
||||
@@ -441,6 +448,7 @@ def url_append_query_for_blender(url: str, blender_version: tuple[int, int, int]
|
||||
query = {
|
||||
"platform": platform_from_this_system(),
|
||||
"blender_version": "{:d}.{:d}.{:d}".format(*blender_version),
|
||||
"python_version": "{:d}.{:d}.{:d}".format(*python_version),
|
||||
}
|
||||
return _url_append_query(url, query)
|
||||
|
||||
@@ -471,7 +479,7 @@ def url_parse_for_blender(url: str) -> tuple[str, dict[str, str]]:
|
||||
for key, value in query:
|
||||
value_xform = None
|
||||
match key:
|
||||
case "blender_version_min" | "blender_version_max" | "platforms":
|
||||
case "blender_version_min" | "blender_version_max" | "python_versions" | "platforms":
|
||||
if value:
|
||||
value_xform = value
|
||||
case "repository":
|
||||
@@ -620,6 +628,7 @@ def pkg_install_files(
|
||||
directory: str,
|
||||
files: Sequence[str],
|
||||
blender_version: tuple[int, int, int],
|
||||
python_version: tuple[int, int, int],
|
||||
use_idle: bool,
|
||||
python_args: Sequence[str],
|
||||
) -> Iterator[InfoItemSeq]:
|
||||
@@ -631,6 +640,7 @@ def pkg_install_files(
|
||||
"install-files", *files,
|
||||
"--local-dir", directory,
|
||||
"--blender-version", "{:d}.{:d}.{:d}".format(*blender_version),
|
||||
"--python-version", "{:d}.{:d}.{:d}".format(*python_version),
|
||||
"--temp-prefix-and-suffix", "/".join(PKG_TEMP_PREFIX_AND_SUFFIX),
|
||||
], use_idle=use_idle, python_args=python_args)
|
||||
yield [COMPLETE_ITEM]
|
||||
@@ -642,6 +652,7 @@ def pkg_install(
|
||||
remote_url: str,
|
||||
pkg_id_sequence: Sequence[str],
|
||||
blender_version: tuple[int, int, int],
|
||||
python_version: tuple[int, int, int],
|
||||
online_user_agent: str,
|
||||
access_token: str,
|
||||
timeout: float,
|
||||
@@ -658,6 +669,7 @@ def pkg_install(
|
||||
"--local-dir", directory,
|
||||
"--remote-url", remote_url,
|
||||
"--blender-version", "{:d}.{:d}.{:d}".format(*blender_version),
|
||||
"--python-version", "{:d}.{:d}.{:d}".format(*python_version),
|
||||
"--online-user-agent", online_user_agent,
|
||||
"--access-token", access_token,
|
||||
"--local-cache", str(int(use_cache)),
|
||||
@@ -819,6 +831,20 @@ def pkg_repo_cache_clear(local_dir: str) -> None:
|
||||
print("Error: unlink", ex)
|
||||
|
||||
|
||||
def python_versions_from_wheel_python_tag(python_tag: str) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
from .cli.blender_ext import python_versions_from_wheel_python_tag as fn
|
||||
result = fn(python_tag)
|
||||
assert isinstance(result, (set, str))
|
||||
return result
|
||||
|
||||
|
||||
def python_versions_from_wheels(wheel_files: Sequence[str]) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
from .cli.blender_ext import python_versions_from_wheels as fn
|
||||
result = fn(wheel_files)
|
||||
assert isinstance(result, (set, str))
|
||||
return result
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Command Pool (non-command-line wrapper)
|
||||
#
|
||||
@@ -1335,6 +1361,7 @@ def repository_id_with_error_fn(
|
||||
class PkgManifest_FilterParams(NamedTuple):
|
||||
platform: str
|
||||
blender_version: tuple[int, int, int]
|
||||
python_version: tuple[int, int, int]
|
||||
|
||||
|
||||
def repository_filter_skip(
|
||||
@@ -1346,6 +1373,7 @@ def repository_filter_skip(
|
||||
result = repository_filter_skip_impl(
|
||||
item,
|
||||
filter_blender_version=filter_params.blender_version,
|
||||
filter_python_version=filter_params.python_version,
|
||||
filter_platform=filter_params.platform,
|
||||
skip_message_fn=None,
|
||||
error_fn=error_fn,
|
||||
@@ -1359,8 +1387,10 @@ def pkg_manifest_params_compatible_or_error(
|
||||
blender_version_min: str,
|
||||
blender_version_max: str,
|
||||
platforms: list[str],
|
||||
python_versions: list[str],
|
||||
this_platform: tuple[int, int, int],
|
||||
this_blender_version: tuple[int, int, int],
|
||||
this_python_version: tuple[int, int, int],
|
||||
error_fn: Callable[[Exception], None],
|
||||
) -> str | None:
|
||||
from .cli.blender_ext import repository_filter_skip as fn
|
||||
@@ -1373,11 +1403,14 @@ def pkg_manifest_params_compatible_or_error(
|
||||
item["blender_version_max"] = blender_version_max
|
||||
if platforms:
|
||||
item["platforms"] = platforms
|
||||
if python_versions:
|
||||
item["python_versions"] = python_versions
|
||||
|
||||
result_report = []
|
||||
result = fn(
|
||||
item=item,
|
||||
filter_blender_version=this_blender_version,
|
||||
filter_python_version=this_python_version,
|
||||
filter_platform=this_platform,
|
||||
# pylint: disable-next=unnecessary-lambda
|
||||
skip_message_fn=lambda msg: result_report.append(msg),
|
||||
@@ -1987,11 +2020,17 @@ class RepoCacheStore:
|
||||
"_is_init",
|
||||
)
|
||||
|
||||
def __init__(self, blender_version: tuple[int, int, int]) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
blender_version: tuple[int, int, int],
|
||||
python_version: tuple[int, int, int],
|
||||
) -> None:
|
||||
self._repos: list[_RepoCacheEntry] = []
|
||||
self._filter_params = PkgManifest_FilterParams(
|
||||
platform=platform_from_this_system(),
|
||||
blender_version=blender_version,
|
||||
python_version=python_version,
|
||||
)
|
||||
self._is_init = False
|
||||
|
||||
|
||||
@@ -104,6 +104,9 @@ RE_MANIFEST_SEMVER = re.compile(
|
||||
# Ensure names (for example), don't contain control characters.
|
||||
RE_CONTROL_CHARS = re.compile(r'[\x00-\x1f\x7f-\x9f]')
|
||||
|
||||
# Use to extract a Python version tag: `py3`, `cp311` etc, from a wheel's filename.
|
||||
RE_PYTHON_WHEEL_VERSION_TAG = re.compile("([a-zA-Z]+)([0-9]+)")
|
||||
|
||||
# Progress updates are displayed after each chunk of this size is downloaded.
|
||||
# Small values add unnecessary overhead showing progress, large values will make
|
||||
# progress not update often enough.
|
||||
@@ -117,6 +120,11 @@ CHUNK_SIZE_DEFAULT = 1 << 14
|
||||
# Used for project tag-line & permissions values.
|
||||
TERSE_DESCRIPTION_MAX_LENGTH = 64
|
||||
|
||||
# Enforce naming spec:
|
||||
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-name-convention
|
||||
# This also defines the name spec:
|
||||
WHEEL_FILENAME_SPEC = "{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl"
|
||||
|
||||
# Default HTML for `server-generate`.
|
||||
# Intentionally very basic, users may define their own `--html-template`.
|
||||
HTML_TEMPLATE = '''\
|
||||
@@ -1871,10 +1879,6 @@ def pkg_manifest_validate_field_wheels(
|
||||
) -> str | None:
|
||||
if (error := pkg_manifest_validate_field_any_list_of_non_empty_strings(value, strict)) is not None:
|
||||
return error
|
||||
# Enforce naming spec:
|
||||
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/#file-name-convention
|
||||
# This also defines the name spec:
|
||||
filename_spec = "{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl"
|
||||
|
||||
for wheel in value:
|
||||
if "\"" in wheel:
|
||||
@@ -1894,7 +1898,10 @@ def pkg_manifest_validate_field_wheels(
|
||||
wheel_filename_split = wheel_filename.split("-")
|
||||
# pylint: disable-next=superfluous-parens
|
||||
if not (5 <= len(wheel_filename_split) <= 6):
|
||||
return "wheel filename must follow the spec \"{:s}\", found {!r}".format(filename_spec, wheel_filename)
|
||||
return "wheel filename must follow the spec \"{:s}\", found {!r}".format(
|
||||
WHEEL_FILENAME_SPEC,
|
||||
wheel_filename,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -2191,6 +2198,99 @@ def blender_platform_compatible_with_wheel_platform_from_filepath(platform: str,
|
||||
return blender_platform_compatible_with_wheel_platform(platform, wheel_platform)
|
||||
|
||||
|
||||
def python_versions_from_wheel_python_tag(python_tag: str) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
"""
|
||||
Return Python versions from a wheels ``python_tag``.
|
||||
"""
|
||||
# Index backwards to skip the optional build tag.
|
||||
# The version may be:
|
||||
# `cp312` for CPython 3.12
|
||||
# `py2.py3` for both Python 2 & 3.
|
||||
versions_string = python_tag.split(".")
|
||||
|
||||
# Based on the documentation as of 2024 and wheels used by existing extensions,
|
||||
# these are the only valid prefix values.
|
||||
version_prefix_known = {"py", "cp"}
|
||||
|
||||
versions: set[tuple[int] | tuple[int, int]] = set()
|
||||
|
||||
for version_string in versions_string:
|
||||
m = RE_PYTHON_WHEEL_VERSION_TAG.match(version_string)
|
||||
if m is None:
|
||||
return "wheel filename version could not be extracted from: \"{:s}\"".format(
|
||||
version_string,
|
||||
)
|
||||
|
||||
version_prefix = m.group(1).lower()
|
||||
version_number = m.group(2)
|
||||
|
||||
if version_prefix in version_prefix_known:
|
||||
if len(version_number) > 1:
|
||||
# Convert: `"311"` to `(3, 11)`.
|
||||
version: tuple[int, int] | tuple[int] = (int(version_number[:1]), int(version_number[1:]))
|
||||
else:
|
||||
# Common for "py3".
|
||||
version = (int(version_number), )
|
||||
versions.add(version)
|
||||
else:
|
||||
return (
|
||||
"wheel filename version prefix failed to be extracted "
|
||||
"found \"{:s}\" int \"{:s}\", expected a value in ({:s})"
|
||||
).format(
|
||||
version_prefix,
|
||||
python_tag,
|
||||
", ".join(sorted(version_prefix_known)),
|
||||
)
|
||||
|
||||
return versions
|
||||
|
||||
|
||||
def python_versions_from_wheel(wheel_filename: str) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
"""
|
||||
Extract a set of Python versions from a list of wheels or return an error string.
|
||||
"""
|
||||
wheel_filename_split = wheel_filename.split("-")
|
||||
|
||||
# pylint: disable-next=superfluous-parens
|
||||
if not (5 <= len(wheel_filename_split) <= 6):
|
||||
return "wheel filename must follow the spec \"{:s}\", found {!r}".format(WHEEL_FILENAME_SPEC, wheel_filename)
|
||||
|
||||
return python_versions_from_wheel_python_tag(wheel_filename_split[-3])
|
||||
|
||||
|
||||
def python_versions_from_wheels(wheel_files: Sequence[str]) -> set[tuple[int] | tuple[int, int]] | str:
|
||||
if not wheel_files:
|
||||
assert False, "unreachable"
|
||||
return ()
|
||||
|
||||
version_major_only: set[tuple[int]] = set()
|
||||
version_major_minor: set[tuple[int, int]] = set()
|
||||
for filepath in wheel_files:
|
||||
if isinstance(result := python_versions_from_wheel(os.path.basename(filepath)), str):
|
||||
return result
|
||||
# Check for an empty set - no version info.
|
||||
if not result:
|
||||
continue
|
||||
|
||||
for v in result:
|
||||
# Exclude support for historic Python versions.
|
||||
# While including this info is technically correct, it's *never* useful to include this info.
|
||||
# So listing this information in every server JSON listing is redundant.
|
||||
if v[0] <= 2:
|
||||
continue
|
||||
|
||||
if len(v) == 1:
|
||||
version_major_only.add(v)
|
||||
else:
|
||||
version_major_minor.add(v)
|
||||
|
||||
# Clean the versions, exclude `(3,)` when `(3, 11)` is present.
|
||||
for v in version_major_minor:
|
||||
version_major_only.discard((v[0],))
|
||||
|
||||
return version_major_only | version_major_minor
|
||||
|
||||
|
||||
def paths_filter_wheels_by_platform(
|
||||
wheels: list[str],
|
||||
platform: str,
|
||||
@@ -2262,6 +2362,7 @@ def repository_filter_skip(
|
||||
*,
|
||||
filter_blender_version: tuple[int, int, int],
|
||||
filter_platform: str,
|
||||
filter_python_version: tuple[int, int, int],
|
||||
# When `skip_message_fn` is set, returning true must call the `skip_message_fn` function.
|
||||
skip_message_fn: Callable[[str], None] | None,
|
||||
error_fn: Callable[[Exception], None],
|
||||
@@ -2278,6 +2379,47 @@ def repository_filter_skip(
|
||||
))
|
||||
return True
|
||||
|
||||
if filter_python_version != (0, 0, 0):
|
||||
if (python_versions := item.get("python_versions")) is not None:
|
||||
if not isinstance(python_versions, list):
|
||||
# Possibly noisy, but this should *not* be happening on a regular basis.
|
||||
error_fn(TypeError("python_versions is not a list, found a: {:s}".format(str(type(python_versions)))))
|
||||
elif python_versions:
|
||||
ok = True
|
||||
python_versions_as_set: set[str] = set()
|
||||
for v in python_versions:
|
||||
if not isinstance(v, str):
|
||||
error_fn(TypeError((
|
||||
"python_versions is not a list of strings, "
|
||||
"found an item of type: {:s}"
|
||||
).format(str(type(v)))))
|
||||
ok = False
|
||||
break
|
||||
# The full Python version isn't explicitly disallowed,
|
||||
# ignore trailing values only check major/minor.
|
||||
if v.count(".") > 1:
|
||||
v = ".".join(v.split(".")[:2])
|
||||
python_versions_as_set.add(v)
|
||||
|
||||
if ok:
|
||||
# There is no need to do any complex extraction and comparison.
|
||||
# Simply check if the `{major}.{minor}` or `{major}` exists in the set.
|
||||
python_versions_as_set = set(python_versions)
|
||||
filter_python_version_major_minor = "{:d}.{:d}".format(*filter_python_version[:2])
|
||||
filter_python_version_major_only = "{:d}".format(filter_python_version[0])
|
||||
if not (
|
||||
filter_python_version_major_minor in python_versions_as_set or
|
||||
filter_python_version_major_only in python_versions_as_set
|
||||
):
|
||||
if skip_message_fn is not None:
|
||||
skip_message_fn("This Python version ({:s}) isn't compatible with ({:s})".format(
|
||||
filter_python_version_major_minor,
|
||||
", ".join(python_versions),
|
||||
))
|
||||
|
||||
return True
|
||||
del filter_python_version_major_minor, filter_python_version_major_only, python_versions_as_set
|
||||
|
||||
if filter_blender_version != (0, 0, 0):
|
||||
version_min_str = item.get("blender_version_min")
|
||||
version_max_str = item.get("blender_version_max")
|
||||
@@ -2326,14 +2468,14 @@ def repository_filter_skip(
|
||||
return False
|
||||
|
||||
|
||||
def blender_version_parse_or_error(version: str) -> tuple[int, int, int] | str:
|
||||
def generic_version_triple_parse_or_error(version: str, identifier: str) -> tuple[int, int, int] | str:
|
||||
try:
|
||||
version_tuple: tuple[int, ...] = tuple(int(x) for x in version.split("."))
|
||||
except Exception as ex:
|
||||
return "unable to parse blender version: {:s}, {:s}".format(version, str(ex))
|
||||
return "unable to parse {:s} version: {:s}, {:s}".format(identifier, version, str(ex))
|
||||
|
||||
if not version_tuple:
|
||||
return "unable to parse empty blender version: {:s}".format(version)
|
||||
return "unable to parse empty {:s} version: {:s}".format(identifier, version)
|
||||
|
||||
# `mypy` can't detect that this is guaranteed to be 3 items.
|
||||
return (
|
||||
@@ -2342,6 +2484,14 @@ def blender_version_parse_or_error(version: str) -> tuple[int, int, int] | str:
|
||||
)
|
||||
|
||||
|
||||
def blender_version_parse_or_error(version: str) -> tuple[int, int, int] | str:
|
||||
return generic_version_triple_parse_or_error(version, "Blender")
|
||||
|
||||
|
||||
def python_version_parse_or_error(version: str) -> tuple[int, int, int] | str:
|
||||
return generic_version_triple_parse_or_error(version, "Python")
|
||||
|
||||
|
||||
def blender_version_parse_any_or_error(version: Any) -> tuple[int, int, int] | str:
|
||||
if not isinstance(version, str):
|
||||
return "blender version should be a string, found a: {:s}".format(str(type(version)))
|
||||
@@ -2433,13 +2583,20 @@ def pkg_manifest_toml_is_valid_or_error(filepath: str, strict: bool) -> tuple[st
|
||||
return None, result
|
||||
|
||||
|
||||
def pkg_manifest_detect_duplicates(pkg_items: list[PkgManifest]) -> str | None:
|
||||
def pkg_manifest_detect_duplicates(
|
||||
pkg_items: list[tuple[
|
||||
PkgManifest,
|
||||
str,
|
||||
list[tuple[int] | tuple[int, int]],
|
||||
]],
|
||||
) -> str | None:
|
||||
"""
|
||||
When a repository includes multiple packages with the same ID, ensure they don't conflict.
|
||||
|
||||
Ensure packages have non-overlapping:
|
||||
- Platforms.
|
||||
- Blender versions.
|
||||
- Python versions.
|
||||
|
||||
Return an error if they do, otherwise None.
|
||||
"""
|
||||
@@ -2448,6 +2605,9 @@ def pkg_manifest_detect_duplicates(pkg_items: list[PkgManifest]) -> str | None:
|
||||
dummy_verion_min = 0, 0, 0
|
||||
dummy_verion_max = 1000, 0, 0
|
||||
|
||||
dummy_platform = ""
|
||||
dummy_python_version = (0,)
|
||||
|
||||
def parse_version_or_default(version: str | None, default: tuple[int, int, int]) -> tuple[int, int, int]:
|
||||
if version is None:
|
||||
return default
|
||||
@@ -2466,68 +2626,175 @@ def pkg_manifest_detect_duplicates(pkg_items: list[PkgManifest]) -> str | None:
|
||||
version_max_str = "..." if dummy_max else "{:d}.{:d}.{:d}".format(*version_max)
|
||||
return "[{:s} -> {:s}]".format(version_min_str, version_max_str)
|
||||
|
||||
# Sort for predictable output.
|
||||
platforms_all = tuple(sorted(set(
|
||||
platform
|
||||
for manifest in pkg_items
|
||||
for platform in (manifest.platforms or ())
|
||||
)))
|
||||
def ordered_int_pair(a: int, b: int) -> tuple[int, int]:
|
||||
assert a != b
|
||||
if a > b:
|
||||
return b, a
|
||||
return a, b
|
||||
|
||||
manifest_per_platform: dict[str, list[PkgManifest]] = {platform: [] for platform in platforms_all}
|
||||
if platforms_all:
|
||||
for manifest in pkg_items:
|
||||
# No platforms means all platforms.
|
||||
for platform in (manifest.platforms or platforms_all):
|
||||
manifest_per_platform[platform].append(manifest)
|
||||
else:
|
||||
manifest_per_platform[""] = pkg_items
|
||||
class PkgManifest_DupeInfo:
|
||||
__slots__ = (
|
||||
"manifest",
|
||||
"filename",
|
||||
"python_versions",
|
||||
"index",
|
||||
|
||||
# Packages have been split by platform, now detect version overlap.
|
||||
platform_dupliates = {}
|
||||
for platform, pkg_items_platform in manifest_per_platform.items():
|
||||
# Must never be empty.
|
||||
assert pkg_items_platform
|
||||
if len(pkg_items_platform) == 1:
|
||||
continue
|
||||
# Version range (min, max) or defaults.
|
||||
"blender_version_range",
|
||||
|
||||
version_ranges: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
|
||||
for manifest in pkg_items_platform:
|
||||
version_ranges.append((
|
||||
# Expand values so duplicates can be detected by comparing literal overlaps.
|
||||
"expanded_platforms",
|
||||
"expanded_python_versions",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
manifest: PkgManifest,
|
||||
filename: str,
|
||||
python_versions: list[tuple[int] | tuple[int, int]],
|
||||
index: int,
|
||||
):
|
||||
self.manifest = manifest
|
||||
self.filename = filename
|
||||
self.python_versions: list[tuple[int] | tuple[int, int]] = python_versions
|
||||
self.index = index
|
||||
|
||||
# NOTE: this assignment could be deferred as it's only needed in the case potential duplicates are found.
|
||||
self.blender_version_range = (
|
||||
parse_version_or_default(manifest.blender_version_min, dummy_verion_min),
|
||||
parse_version_or_default(manifest.blender_version_max, dummy_verion_max),
|
||||
))
|
||||
# Sort by the version range so overlaps can be detected between adjacent members.
|
||||
version_ranges.sort()
|
||||
)
|
||||
|
||||
duplicates_found = []
|
||||
item_prev = version_ranges[0]
|
||||
for i in range(1, len(version_ranges)):
|
||||
item_curr = version_ranges[i]
|
||||
self.expanded_platforms: list[str] = []
|
||||
self.expanded_python_versions: list[tuple[int] | tuple[int, int]] = []
|
||||
|
||||
pkg_items_dup = [
|
||||
PkgManifest_DupeInfo(
|
||||
manifest=manifest,
|
||||
filename=filename,
|
||||
python_versions=python_versions,
|
||||
index=i,
|
||||
) for i, (manifest, filename, python_versions) in enumerate(pkg_items)
|
||||
]
|
||||
|
||||
# Store all configurations.
|
||||
platforms_all = set()
|
||||
python_versions_all = set()
|
||||
|
||||
for item_dup in pkg_items_dup:
|
||||
item = item_dup.manifest
|
||||
if item.platforms:
|
||||
platforms_all.update(item.platforms)
|
||||
|
||||
if item_dup.python_versions:
|
||||
python_versions_all.update(item_dup.python_versions)
|
||||
del item
|
||||
|
||||
# Expand values.
|
||||
for item_dup in pkg_items_dup:
|
||||
item = item_dup.manifest
|
||||
if platforms_all:
|
||||
item_dup.expanded_platforms[:] = item.platforms or platforms_all
|
||||
else:
|
||||
item_dup.expanded_platforms[:] = [dummy_platform]
|
||||
|
||||
if python_versions_all:
|
||||
item_dup.expanded_python_versions[:] = item_dup.python_versions or python_versions_all
|
||||
else:
|
||||
item_dup.expanded_python_versions[:] = [dummy_python_version]
|
||||
del item
|
||||
|
||||
# Expand Python "major only" versions.
|
||||
# Some wheels define "py3" only, this will be something we have to deal with
|
||||
# when Python moves to Python 4.
|
||||
# It's important that Python 3.11 detects as conflicting with Python 3.
|
||||
python_versions_all_map_major_to_full: dict[int, list[tuple[int, int]]] = {}
|
||||
for python_version in python_versions_all:
|
||||
if len(python_version) != 2:
|
||||
continue
|
||||
python_version_major = python_version[0]
|
||||
if (python_version_major,) not in python_versions_all:
|
||||
continue
|
||||
|
||||
if (python_versions := python_versions_all_map_major_to_full.get(python_version_major)) is None:
|
||||
python_versions = python_versions_all_map_major_to_full[python_version_major] = []
|
||||
python_versions.append(python_version)
|
||||
del python_versions
|
||||
|
||||
for item_dup in pkg_items_dup:
|
||||
for python_version in item_dup.python_versions:
|
||||
if len(python_version) != 1:
|
||||
continue
|
||||
python_version_major = python_version[0]
|
||||
|
||||
expanded_python_versions_set = set(item_dup.expanded_python_versions)
|
||||
if (python_versions_full := python_versions_all_map_major_to_full.get(python_version_major)) is not None:
|
||||
# Expand major to major-minor versions.
|
||||
item_dup.expanded_python_versions.extend([
|
||||
v for v in python_versions_full
|
||||
if v not in expanded_python_versions_set
|
||||
])
|
||||
del python_versions_full
|
||||
|
||||
# This can be expanded with additional values as needed.
|
||||
# We could in principle have ABI flags (debug/release) for e.g.
|
||||
PkgCfgKey = tuple[
|
||||
# Platform.
|
||||
str,
|
||||
# Python Version.
|
||||
tuple[int] | tuple[int, int],
|
||||
]
|
||||
|
||||
pkg_items_dup_per_cfg: dict[PkgCfgKey, list[PkgManifest_DupeInfo]] = {}
|
||||
|
||||
for item_dup in pkg_items_dup:
|
||||
for platform in item_dup.expanded_platforms:
|
||||
for python_version in item_dup.expanded_python_versions:
|
||||
key = (platform, python_version)
|
||||
if (pkg_items_cfg := pkg_items_dup_per_cfg.get(key)) is None:
|
||||
pkg_items_cfg = pkg_items_dup_per_cfg[key] = []
|
||||
pkg_items_cfg.append(item_dup)
|
||||
del pkg_items_cfg
|
||||
|
||||
# Don't report duplicates more than once.
|
||||
duplicate_indices: set[tuple[int, int]] = set()
|
||||
|
||||
# Packages have been split by configuration, now detect version overlap.
|
||||
duplicates_found = []
|
||||
|
||||
# NOTE: we might want to include the `key` in the message,
|
||||
# after all, it's useful to know if the conflict occurs between specific platforms or Python versions.
|
||||
|
||||
for pkg_items_cfg in pkg_items_dup_per_cfg.values():
|
||||
# Must never be empty.
|
||||
assert pkg_items_cfg
|
||||
if len(pkg_items_cfg) == 1:
|
||||
continue
|
||||
|
||||
# Sort by the version range so overlaps can be detected between adjacent members.
|
||||
pkg_items_cfg.sort(key=lambda item_dup: item_dup.blender_version_range)
|
||||
|
||||
item_prev = pkg_items_cfg[0]
|
||||
for i in range(1, len(pkg_items_cfg)):
|
||||
item_curr = pkg_items_cfg[i]
|
||||
|
||||
# Previous maximum is less than or equal to the current minimum, no overlap.
|
||||
if item_prev[1] > item_curr[0]:
|
||||
duplicates_found.append("{:s} & {:s}".format(
|
||||
version_range_as_str(*item_prev),
|
||||
version_range_as_str(*item_curr),
|
||||
))
|
||||
if item_prev.blender_version_range[1] > item_curr.blender_version_range[0]:
|
||||
# Don't report multiple times.
|
||||
dup_key = ordered_int_pair(item_prev.index, item_curr.index)
|
||||
if dup_key not in duplicate_indices:
|
||||
duplicate_indices.add(dup_key)
|
||||
duplicates_found.append("{:s}={:s} & {:s}={:s}".format(
|
||||
item_prev.filename, version_range_as_str(*item_prev.blender_version_range),
|
||||
item_curr.filename, version_range_as_str(*item_curr.blender_version_range),
|
||||
))
|
||||
item_prev = item_curr
|
||||
|
||||
if duplicates_found:
|
||||
platform_dupliates[platform] = duplicates_found
|
||||
|
||||
if platform_dupliates:
|
||||
# Simpler, no platforms.
|
||||
if platforms_all:
|
||||
error_text = ", ".join([
|
||||
"\"{:s}\": ({:s})".format(platform, ", ".join(errors))
|
||||
for platform, errors in platform_dupliates.items()
|
||||
])
|
||||
else:
|
||||
error_text = ", ".join(platform_dupliates[""])
|
||||
|
||||
if duplicates_found:
|
||||
return "{:d} duplicate(s) found, conflicting blender versions {:s}".format(
|
||||
sum(map(len, platform_dupliates.values())),
|
||||
error_text,
|
||||
len(duplicates_found),
|
||||
", ".join(duplicates_found),
|
||||
)
|
||||
|
||||
# No collisions found.
|
||||
@@ -3020,6 +3287,19 @@ def generic_arg_blender_version(subparse: argparse.ArgumentParser) -> None:
|
||||
)
|
||||
|
||||
|
||||
def generic_arg_python_version(subparse: argparse.ArgumentParser) -> None:
|
||||
subparse.add_argument(
|
||||
"--python-version",
|
||||
dest="python_version",
|
||||
default="0.0.0",
|
||||
type=str,
|
||||
help=(
|
||||
"The version of Python used for selecting packages."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
def generic_arg_temp_prefix_and_suffix(subparse: argparse.ArgumentParser) -> None:
|
||||
subparse.add_argument(
|
||||
"--temp-prefix-and-suffix",
|
||||
@@ -3262,6 +3542,7 @@ class subcmd_server:
|
||||
fh.write(" <th>Description</th>\n")
|
||||
fh.write(" <th>Website</th>\n")
|
||||
fh.write(" <th>Blender Versions</th>\n")
|
||||
fh.write(" <th>Python Versions</th>\n")
|
||||
fh.write(" <th>Platforms</th>\n")
|
||||
fh.write(" <th>Size</th>\n")
|
||||
fh.write(" </tr>\n")
|
||||
@@ -3273,6 +3554,7 @@ class subcmd_server:
|
||||
fh.write(" <tr>\n")
|
||||
|
||||
platforms = manifest_dict.get("platforms", [])
|
||||
python_versions = manifest_dict.get("python_versions", [])
|
||||
|
||||
# Parse the URL and add parameters use for drag & drop.
|
||||
parsed_url = urllib.parse.urlparse(manifest_dict["archive_url"])
|
||||
@@ -3285,6 +3567,8 @@ class subcmd_server:
|
||||
query["blender_version_max"] = value
|
||||
if platforms:
|
||||
query["platforms"] = ",".join(platforms)
|
||||
if python_versions:
|
||||
query["python_versions"] = ",".join(python_versions)
|
||||
del value
|
||||
|
||||
id_and_link = "<a href=\"{:s}\">{:s}</a>".format(
|
||||
@@ -3317,7 +3601,14 @@ class subcmd_server:
|
||||
)
|
||||
else:
|
||||
blender_version_str = "all"
|
||||
|
||||
if python_versions:
|
||||
python_version_str = ", ".join(python_versions)
|
||||
else:
|
||||
python_version_str = "all"
|
||||
|
||||
fh.write(" <td>{:s}</td>\n".format(html.escape(blender_version_str)))
|
||||
fh.write(" <td>{:s}</td>\n".format(html.escape(python_version_str)))
|
||||
fh.write(" <td>{:s}</td>\n".format(html.escape(", ".join(platforms) if platforms else "all")))
|
||||
fh.write(" <td>{:s}</td>\n".format(html.escape(size_as_fmt_string(manifest_dict["archive_size"]))))
|
||||
fh.write(" </tr>\n")
|
||||
@@ -3403,7 +3694,7 @@ class subcmd_server:
|
||||
return False
|
||||
assert repo_config is None or isinstance(repo_config, PkgServerRepoConfig)
|
||||
|
||||
repo_data_idname_map: dict[str, list[PkgManifest]] = {}
|
||||
repo_data_idname_map: dict[str, list[tuple[PkgManifest, str, list[tuple[int] | tuple[int, int]]]]] = {}
|
||||
repo_data: list[dict[str, Any]] = []
|
||||
|
||||
# Write package meta-data into each directory.
|
||||
@@ -3436,9 +3727,6 @@ class subcmd_server:
|
||||
manifest_dict = manifest._asdict()
|
||||
|
||||
pkg_idname = manifest_dict["id"]
|
||||
if (pkg_items := repo_data_idname_map.get(pkg_idname)) is None:
|
||||
pkg_items = repo_data_idname_map[pkg_idname] = []
|
||||
pkg_items.append(manifest)
|
||||
|
||||
# Call all optional keys so the JSON never contains `null` items.
|
||||
for key, value in list(manifest_dict.items()):
|
||||
@@ -3446,8 +3734,27 @@ class subcmd_server:
|
||||
del manifest_dict[key]
|
||||
|
||||
# Don't include these in the server listing.
|
||||
for key in ("wheels", ):
|
||||
manifest_dict.pop(key, None)
|
||||
wheels: list[str] = manifest_dict.pop("wheels", [])
|
||||
|
||||
# Extract the `python_versions` from wheels.
|
||||
python_versions_final: list[tuple[int] | tuple[int, int]] = []
|
||||
if wheels:
|
||||
if isinstance(python_versions := python_versions_from_wheels(wheels), str):
|
||||
msglog.warn("unable to parse Python version from \"wheels\" ({:s}): {:s}".format(
|
||||
python_versions,
|
||||
filepath,
|
||||
))
|
||||
else:
|
||||
python_versions_final[:] = sorted(python_versions)
|
||||
|
||||
manifest_dict["python_versions"] = [
|
||||
".".join(str(v) for v in version)
|
||||
for version in python_versions_final
|
||||
]
|
||||
|
||||
if (pkg_items := repo_data_idname_map.get(pkg_idname)) is None:
|
||||
pkg_items = repo_data_idname_map[pkg_idname] = []
|
||||
pkg_items.append((manifest, filename, python_versions_final))
|
||||
|
||||
# These are added, ensure they don't exist.
|
||||
has_key_error = False
|
||||
@@ -3476,6 +3783,8 @@ class subcmd_server:
|
||||
for pkg_idname, pkg_items in repo_data_idname_map.items():
|
||||
if len(pkg_items) == 1:
|
||||
continue
|
||||
# Sort for predictable output.
|
||||
pkg_items.sort(key=lambda pkg_item_ext: pkg_item_ext[1])
|
||||
if (error := pkg_manifest_detect_duplicates(pkg_items)) is not None:
|
||||
msglog.warn("archive found with duplicates for id {:s}: {:s}".format(pkg_idname, error))
|
||||
|
||||
@@ -3614,6 +3923,7 @@ class subcmd_client:
|
||||
local_dir: str,
|
||||
filepath_archive: str,
|
||||
blender_version_tuple: tuple[int, int, int],
|
||||
python_version_tuple: tuple[int, int, int],
|
||||
manifest_compare: PkgManifest | None,
|
||||
temp_prefix_and_suffix: tuple[str, str],
|
||||
) -> bool:
|
||||
@@ -3670,6 +3980,7 @@ class subcmd_client:
|
||||
manifest._asdict(),
|
||||
filter_blender_version=blender_version_tuple,
|
||||
filter_platform=platform_from_this_system(),
|
||||
filter_python_version=python_version_tuple,
|
||||
skip_message_fn=lambda message: any_as_none(
|
||||
msglog.error("{:s}: {:s}".format(manifest.id, message))
|
||||
),
|
||||
@@ -3736,6 +4047,7 @@ class subcmd_client:
|
||||
local_dir: str,
|
||||
package_files: Sequence[str],
|
||||
blender_version: str,
|
||||
python_version: str,
|
||||
temp_prefix_and_suffix: tuple[str, str],
|
||||
) -> bool:
|
||||
if not os.path.exists(local_dir):
|
||||
@@ -3747,6 +4059,11 @@ class subcmd_client:
|
||||
return False
|
||||
assert isinstance(blender_version_tuple, tuple)
|
||||
|
||||
if isinstance(python_version_tuple := python_version_parse_or_error(python_version), str):
|
||||
msglog.fatal_error(python_version_tuple)
|
||||
return False
|
||||
assert isinstance(python_version_tuple, tuple)
|
||||
|
||||
# This is a simple file extraction, the main difference is that it validates the manifest before installing.
|
||||
directories_to_clean: list[str] = []
|
||||
with CleanupPathsContext(files=(), directories=directories_to_clean):
|
||||
@@ -3756,6 +4073,7 @@ class subcmd_client:
|
||||
local_dir=local_dir,
|
||||
filepath_archive=filepath_archive,
|
||||
blender_version_tuple=blender_version_tuple,
|
||||
python_version_tuple=python_version_tuple,
|
||||
# There is no manifest from the repository, leave this unset.
|
||||
manifest_compare=None,
|
||||
temp_prefix_and_suffix=temp_prefix_and_suffix,
|
||||
@@ -3775,6 +4093,7 @@ class subcmd_client:
|
||||
packages: Sequence[str],
|
||||
online_user_agent: str,
|
||||
blender_version: str,
|
||||
python_version: str,
|
||||
access_token: str,
|
||||
timeout_in_seconds: float,
|
||||
temp_prefix_and_suffix: tuple[str, str],
|
||||
@@ -3790,6 +4109,11 @@ class subcmd_client:
|
||||
return False
|
||||
assert isinstance(blender_version_tuple, tuple)
|
||||
|
||||
if isinstance(python_version_tuple := python_version_parse_or_error(python_version), str):
|
||||
msglog.fatal_error(python_version_tuple)
|
||||
return False
|
||||
assert isinstance(python_version_tuple, tuple)
|
||||
|
||||
# Extract...
|
||||
if isinstance((pkg_repo_data := repo_pkginfo_from_local_or_none(local_dir=local_dir)), str):
|
||||
msglog.fatal_error("Error loading package repository: {:s}".format(pkg_repo_data))
|
||||
@@ -3854,6 +4178,7 @@ class subcmd_client:
|
||||
pkg_info,
|
||||
filter_blender_version=blender_version_tuple,
|
||||
filter_platform=platform_this,
|
||||
filter_python_version=python_version_tuple,
|
||||
skip_message_fn=None,
|
||||
error_fn=lambda ex: any_as_none(
|
||||
# pylint: disable-next=cell-var-from-loop
|
||||
@@ -4002,6 +4327,7 @@ class subcmd_client:
|
||||
local_dir=local_dir,
|
||||
filepath_archive=filepath_local_cache_archive,
|
||||
blender_version_tuple=blender_version_tuple,
|
||||
python_version_tuple=python_version_tuple,
|
||||
manifest_compare=manifest_archive.manifest,
|
||||
temp_prefix_and_suffix=temp_prefix_and_suffix,
|
||||
):
|
||||
@@ -4813,6 +5139,7 @@ def argparse_create_client_install_files(subparsers: "argparse._SubParsersAction
|
||||
|
||||
generic_arg_local_dir(subparse)
|
||||
generic_arg_blender_version(subparse)
|
||||
generic_arg_python_version(subparse)
|
||||
|
||||
generic_arg_temp_prefix_and_suffix(subparse)
|
||||
|
||||
@@ -4824,6 +5151,7 @@ def argparse_create_client_install_files(subparsers: "argparse._SubParsersAction
|
||||
local_dir=args.local_dir,
|
||||
package_files=args.files,
|
||||
blender_version=args.blender_version,
|
||||
python_version=args.python_version,
|
||||
temp_prefix_and_suffix=args.temp_prefix_and_suffix,
|
||||
),
|
||||
)
|
||||
@@ -4843,6 +5171,7 @@ def argparse_create_client_install(subparsers: "argparse._SubParsersAction[argpa
|
||||
generic_arg_local_cache(subparse)
|
||||
generic_arg_online_user_agent(subparse)
|
||||
generic_arg_blender_version(subparse)
|
||||
generic_arg_python_version(subparse)
|
||||
generic_arg_access_token(subparse)
|
||||
|
||||
generic_arg_temp_prefix_and_suffix(subparse)
|
||||
@@ -4859,6 +5188,7 @@ def argparse_create_client_install(subparsers: "argparse._SubParsersAction[argpa
|
||||
packages=args.packages.split(","),
|
||||
online_user_agent=args.online_user_agent,
|
||||
blender_version=args.blender_version,
|
||||
python_version=args.python_version,
|
||||
access_token=args.access_token,
|
||||
timeout_in_seconds=args.timeout,
|
||||
temp_prefix_and_suffix=args.temp_prefix_and_suffix,
|
||||
|
||||
@@ -33,7 +33,7 @@ JSON_OutputElem = tuple[str, Any]
|
||||
|
||||
# For more useful output that isn't clipped.
|
||||
# pylint: disable-next=protected-access
|
||||
unittest.util._MAX_LENGTH = 10_000
|
||||
unittest.util._MAX_LENGTH = 10_000 # type: ignore
|
||||
|
||||
IS_WIN32 = sys.platform == "win32"
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ from collections.abc import (
|
||||
|
||||
# For more useful output that isn't clipped.
|
||||
# pylint: disable-next=protected-access
|
||||
unittest.util._MAX_LENGTH = 10_000
|
||||
unittest.util._MAX_LENGTH = 10_000 # type: ignore
|
||||
|
||||
|
||||
PKG_EXT = ".zip"
|
||||
|
||||
@@ -131,6 +132,8 @@ class WheelModuleParams(NamedTuple):
|
||||
module_name: str
|
||||
module_version: str
|
||||
|
||||
filename: str | None = None
|
||||
|
||||
|
||||
def path_to_url(path: str) -> str:
|
||||
from urllib.parse import urljoin
|
||||
@@ -171,7 +174,7 @@ def create_package(
|
||||
pkg_idname: str,
|
||||
|
||||
# Optional.
|
||||
wheel_params: WheelModuleParams | None = None,
|
||||
wheel_params: Sequence[WheelModuleParams] = (),
|
||||
platforms: tuple[str, ...] | None = None,
|
||||
blender_version_min: str | None = None,
|
||||
blender_version_max: str | None = None,
|
||||
@@ -180,22 +183,28 @@ def create_package(
|
||||
) -> None:
|
||||
pkg_name = pkg_idname.replace("_", " ").title()
|
||||
|
||||
if wheel_params is not None:
|
||||
wheel_filename, wheel_filedata = python_wheel_generate.generate_from_source(
|
||||
module_name=wheel_params.module_name,
|
||||
version=wheel_params.module_version,
|
||||
source=(
|
||||
"__version__ = {!r}\n"
|
||||
"print(\"The wheel has been found\")\n"
|
||||
).format(wheel_params.module_version),
|
||||
)
|
||||
wheel_filenames = []
|
||||
if wheel_params:
|
||||
for w in wheel_params:
|
||||
wheel_filename, wheel_filedata = python_wheel_generate.generate_from_source(
|
||||
module_name=w.module_name,
|
||||
version=w.module_version,
|
||||
source=(
|
||||
"__version__ = {!r}\n"
|
||||
"print(\"The wheel has been found\")\n"
|
||||
).format(w.module_version),
|
||||
)
|
||||
if w.filename is not None:
|
||||
wheel_filename = w.filename
|
||||
|
||||
wheel_dir = os.path.join(pkg_src_dir, "wheels")
|
||||
os.makedirs(wheel_dir, exist_ok=True)
|
||||
wheel_dir = os.path.join(pkg_src_dir, "wheels")
|
||||
os.makedirs(wheel_dir, exist_ok=True)
|
||||
|
||||
wheel_path = os.path.join(wheel_dir, wheel_filename)
|
||||
with open(wheel_path, "wb") as fh:
|
||||
fh.write(wheel_filedata)
|
||||
wheel_path = os.path.join(wheel_dir, wheel_filename)
|
||||
with open(wheel_path, "wb") as fh:
|
||||
fh.write(wheel_filedata)
|
||||
|
||||
wheel_filenames.append(wheel_filename)
|
||||
|
||||
with open(os.path.join(pkg_src_dir, PKG_MANIFEST_FILENAME_TOML), "w", encoding="utf-8") as fh:
|
||||
fh.write('''# Example\n''')
|
||||
@@ -213,15 +222,18 @@ def create_package(
|
||||
fh.write('''blender_version_max = "{:s}"\n'''.format(blender_version_max))
|
||||
fh.write('''\n''')
|
||||
|
||||
if wheel_params is not None:
|
||||
fh.write('''wheels = ["./wheels/{:s}"]\n'''.format(wheel_filename))
|
||||
if wheel_filenames:
|
||||
fh.write('''wheels = [''')
|
||||
for wheel_filename in wheel_filenames:
|
||||
fh.write(''' "./wheels/{:s}",\n'''.format(wheel_filename))
|
||||
fh.write(''']\n''')
|
||||
|
||||
if platforms is not None:
|
||||
fh.write('''platforms = [{:s}]\n'''.format(", ".join(["\"{:s}\"".format(x) for x in platforms])))
|
||||
|
||||
with open(os.path.join(pkg_src_dir, "__init__.py"), "w", encoding="utf-8") as fh:
|
||||
if wheel_params is not None:
|
||||
fh.write("import {:s}\n".format(wheel_params.module_name))
|
||||
for w in wheel_params:
|
||||
fh.write("import {:s}\n".format(w.module_name))
|
||||
|
||||
if python_script is not None:
|
||||
fh.write(python_script)
|
||||
@@ -408,7 +420,7 @@ class TestWithTempBlenderUser_MixIn(unittest.TestCase):
|
||||
self,
|
||||
*,
|
||||
pkg_idname: str,
|
||||
wheel_params: WheelModuleParams | None = None,
|
||||
wheel_params: Sequence[WheelModuleParams] = (),
|
||||
|
||||
# Optional.
|
||||
pkg_filename: str | None = None,
|
||||
@@ -467,9 +479,11 @@ class TestSimple(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
pkg_idname = "my_test_pkg"
|
||||
self.build_package(
|
||||
pkg_idname=pkg_idname,
|
||||
wheel_params=WheelModuleParams(
|
||||
module_name=wheel_module_name,
|
||||
module_version="1.0.1",
|
||||
wheel_params=(
|
||||
WheelModuleParams(
|
||||
module_name=wheel_module_name,
|
||||
module_version="1.0.1",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -554,9 +568,11 @@ class TestSimple(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
packages_to_install.append(pkg_idname)
|
||||
self.build_package(
|
||||
pkg_idname=pkg_idname,
|
||||
wheel_params=WheelModuleParams(
|
||||
module_name=wheel_module_name,
|
||||
module_version=wheel_module_version,
|
||||
wheel_params=(
|
||||
WheelModuleParams(
|
||||
module_name=wheel_module_name,
|
||||
module_version=wheel_module_version,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -755,23 +771,150 @@ class TestPlatform(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
"--repo-dir", TEMP_DIR_REMOTE,
|
||||
))
|
||||
self.assertEqual(stdout, (
|
||||
'''WARN: archive found with duplicates for id {pkg_idname:s}: '''
|
||||
'''3 duplicate(s) found, conflicting blender versions \"{platform:s}\": '''
|
||||
'''([undefined] & [{version_a:s} -> {version_b:s}], '''
|
||||
'''[{version_a:s} -> {version_b:s}] & [{version_a:s} -> {version_e:s}], '''
|
||||
'''[{version_a:s} -> {version_e:s}] & [{version_c:s} -> {version_d:s}])\n'''
|
||||
'''WARN: archive found with duplicates for id my_platform_test: '''
|
||||
'''3 duplicate(s) found, conflicting blender versions '''
|
||||
'''my_platform_test-linux_x64_conflict_no_version.zip=[undefined] & '''
|
||||
'''my_platform_test-linux_x64_no_conflict_old.zip=[2.3.0 -> 3.3.0], '''
|
||||
'''my_platform_test-linux_x64_no_conflict_old.zip=[2.3.0 -> 3.3.0] & '''
|
||||
'''my_platform_test-linux_x64_conflict.zip=[2.3.0 -> 4.5.0], '''
|
||||
'''my_platform_test-linux_x64_conflict.zip=[2.3.0 -> 4.5.0] & '''
|
||||
'''my_platform_test-linux_x64.zip=[4.3.0 -> 4.4.0]\n'''
|
||||
'''found 7 packages.\n'''
|
||||
).format(
|
||||
pkg_idname=pkg_idname,
|
||||
platform=platform_this,
|
||||
version_a=version_a,
|
||||
version_b=version_b,
|
||||
version_c=version_c_this,
|
||||
version_d=version_d,
|
||||
version_e=version_e,
|
||||
))
|
||||
|
||||
|
||||
# While other tests use this command, focus on testing specific behaviors work as expected.
|
||||
class TestPythonVersions(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
|
||||
def test_server_generate_version_with_dupes(self) -> None:
|
||||
repo_id = "test_repo_blocklist"
|
||||
repo_name = "MyTestRepoServerGenerate"
|
||||
|
||||
self.repo_add(repo_id=repo_id, repo_name=repo_name)
|
||||
|
||||
pkg_idnames = (
|
||||
("my_test_pkg", "my_test_a", "example-1.2.3-cp311-cp311-any.whl"),
|
||||
("my_test_pkg", "my_test_b", "example-1.2.3-cp311-cp311-any.whl"),
|
||||
("my_test_pkg", "my_test_c", "example-1.2.3-cp311-cp311-any.whl"),
|
||||
)
|
||||
|
||||
# Create a package contents.
|
||||
for pkg_idname, pkg_filename, wheel_filename in pkg_idnames:
|
||||
self.build_package(
|
||||
pkg_idname=pkg_idname,
|
||||
pkg_filename=pkg_filename,
|
||||
blender_version_min="4.2.0",
|
||||
blender_version_max="4.3.0",
|
||||
wheel_params=(
|
||||
WheelModuleParams(
|
||||
module_name="example",
|
||||
module_version="1.0.1",
|
||||
filename=wheel_filename,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Generate the repository.
|
||||
returncode, stdout, stderr = run_blender_extensions((
|
||||
"server-generate",
|
||||
"--repo-dir", TEMP_DIR_REMOTE,
|
||||
))
|
||||
self.assertEqual(stderr, "")
|
||||
self.assertEqual(returncode, 0)
|
||||
self.assertEqual(
|
||||
stdout,
|
||||
(
|
||||
'''WARN: archive found with duplicates for id my_test_pkg: '''
|
||||
'''2 duplicate(s) found, conflicting blender versions '''
|
||||
'''my_test_a.zip=[4.2.0 -> 4.3.0] & my_test_b.zip=[4.2.0 -> 4.3.0], '''
|
||||
'''my_test_b.zip=[4.2.0 -> 4.3.0] & my_test_c.zip=[4.2.0 -> 4.3.0]\n'''
|
||||
'''found 3 packages.\n'''
|
||||
),
|
||||
)
|
||||
|
||||
def test_server_generate_version_with_dupes_major_only_mix(self) -> None:
|
||||
repo_id = "test_repo_blocklist"
|
||||
repo_name = "MyTestRepoServerGenerate"
|
||||
|
||||
self.repo_add(repo_id=repo_id, repo_name=repo_name)
|
||||
|
||||
pkg_idnames = (
|
||||
("my_test_pkg", "my_test_a", "example-1.2.3-cp311-cp311-any.whl"),
|
||||
("my_test_pkg", "my_test_b", "example-1.2.3-py3.py4-py3.py4-any.whl"),
|
||||
)
|
||||
|
||||
# Create a package contents.
|
||||
for pkg_idname, pkg_filename, wheel_filename in pkg_idnames:
|
||||
self.build_package(
|
||||
pkg_idname=pkg_idname,
|
||||
pkg_filename=pkg_filename,
|
||||
blender_version_min="4.2.0",
|
||||
blender_version_max="4.3.0",
|
||||
wheel_params=(
|
||||
WheelModuleParams(
|
||||
module_name="example",
|
||||
module_version="1.0.1",
|
||||
filename=wheel_filename,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Generate the repository.
|
||||
returncode, stdout, stderr = run_blender_extensions((
|
||||
"server-generate",
|
||||
"--repo-dir", TEMP_DIR_REMOTE,
|
||||
))
|
||||
self.assertEqual(stderr, "")
|
||||
self.assertEqual(returncode, 0)
|
||||
self.assertEqual(
|
||||
stdout,
|
||||
(
|
||||
'''WARN: archive found with duplicates for id my_test_pkg: '''
|
||||
'''1 duplicate(s) found, conflicting blender versions '''
|
||||
'''my_test_a.zip=[4.2.0 -> 4.3.0] & my_test_b.zip=[4.2.0 -> 4.3.0]\n'''
|
||||
'''found 2 packages.\n'''
|
||||
),
|
||||
)
|
||||
|
||||
def test_server_generate_version_without_dupes(self) -> None:
|
||||
# The different Python versions in the wheels cause the packages not to conflict.
|
||||
repo_id = "test_repo_blocklist"
|
||||
repo_name = "MyTestRepoServerGenerate"
|
||||
|
||||
self.repo_add(repo_id=repo_id, repo_name=repo_name)
|
||||
|
||||
pkg_idnames = (
|
||||
("my_test_pkg", "my_test_a", "example-1.2.3-cp311-cp311-any.whl"),
|
||||
("my_test_pkg", "my_test_b", "example-1.2.3-cp312-cp312-any.whl"),
|
||||
("my_test_pkg", "my_test_c", "example-1.2.3-cp313-cp313-any.whl"),
|
||||
)
|
||||
|
||||
# Create a package contents.
|
||||
for pkg_idname, pkg_filename, wheel_filename in pkg_idnames:
|
||||
self.build_package(
|
||||
pkg_idname=pkg_idname,
|
||||
pkg_filename=pkg_filename,
|
||||
blender_version_min="4.2.0",
|
||||
blender_version_max="4.3.0",
|
||||
wheel_params=(
|
||||
WheelModuleParams(
|
||||
module_name="example",
|
||||
module_version="1.0.1",
|
||||
filename=wheel_filename,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# Generate the repository.
|
||||
returncode, stdout, stderr = run_blender_extensions((
|
||||
"server-generate",
|
||||
"--repo-dir", TEMP_DIR_REMOTE,
|
||||
))
|
||||
self.assertEqual(returncode, 0)
|
||||
self.assertEqual(stderr, "")
|
||||
self.assertEqual(stdout, '''found 3 packages.\n''')
|
||||
|
||||
|
||||
class TestModuleViolation(TestWithTempBlenderUser_MixIn, unittest.TestCase):
|
||||
|
||||
def test_extension(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user